---

# Introduction

# Introduction

> Overview of AudioUI, a React component library for building audio and MIDI application interfaces.

AudioUI is a React component library designed for building user interfaces in audio and MIDI applications. It provides a set of components optimized for the performance and interaction patterns required by music software, digital audio workstations (DAWs), audio plugins with web UIs, and other audio-centric applications.

> **Want to jump in?:** If you prefer to go straight to the point or like exploring by doing, head over to the [Playground app](https://playground.cutoff.dev). It showcases AudioUI capabilities and contains many examples you can try live.

## Design Philosophy

AudioUI is built around core principles that address the unique challenges of audio software development:

### Performance First

Audio applications have strict performance requirements. Components are optimized for minimal re-renders and low-latency interactions to ensure the UI remains responsive, even under heavy load.

### Hybrid Architecture

The library provides both opinionated components for rapid development and non-opinionated primitives for deep customization:

* **Opinionated Components**: Ready-to-use components like Knob, Slider, Button, CycleButton, and Keys with carefully designed APIs, multiple visual variants, and sensible defaults. These components are based on thorough observation of existing audio apps and plugins, built with the intent of covering 90% of the needs of most audio app and plugin developers. They are themable using a ThemeManager, can be fine-tuned using styles and props, and provide several variants per component to match different visual styles.
* **Non-Opinionated Components**: Generic control components, SVG primitives, and base building blocks that enable full customization. Ideal for creating unique visual designs that match your brand or existing plugin aesthetics. The [customization page](https://playground.cutoff.dev/examples/customization) contains multiple examples of totally customized components built with these primitives, and its source code is available for reference.

### Universal Access

AudioUI is designed from the ground up to be fully accessible with ARIA support and keyboard navigation, and mobile-ready with responsive, touch-optimized interactions.

### Developer Experience

The library is written entirely in TypeScript for type safety and provides a flexible theming system through a ThemeManager. The theme system currently allows you to define a primary color and a roundness attribute, with more theme options planned for the 1.0 release (including glowness, reflectiveness, and more). For an overview of theming on vector components, see [Theming Features](https://cutoff.dev/audio-ui/docs/latest/components/vector/introduction#theming-features) in the Vector introduction. The architecture separates framework-agnostic core logic from React-specific implementations, enabling potential support for other frameworks in the future.

## Project Status

**Developer Preview** — AudioUI is currently in active development. The core components and APIs are functional and ready for use, but the library is still evolving.

**Current Status:**

* Core component APIs (Knob, Slider, Button, CycleButton, Keys) are stable and production-ready
* Comprehensive audio parameters model with support for Continuous, Discrete, and Boolean controls
* Generic film strip and bitmap image components for each type of audio parameter
* Comprehensive interaction system for each type of parameter (drag, wheel, keyboard)
* Layout system with adaptive sizing
* Theming and customization system with SVG primitives and base components
* Built-in support for dark and light mode
* Ready for mobile devices (responsive, touch ready)

**In Progress:**

* Additional visual variants for CycleButton and Button (Knob and Slider already have multiple variants available)
* Size system improvements
* Documentation expansion

**Planned for 1.0:**

* Additional components: XY Pad, Pitch/Mod Wheels and Levers, and an Icon library dedicated to audio and MIDI apps and plugins
* Enhanced theme system with glowness, reflectiveness, and more visual attributes

As a Developer Preview release, breaking changes may occur as we refine the API and architecture. We recommend pinning to specific versions for production use.

## Core Components

AudioUI provides a range of components essential for building audio applications:

### Built-in Controls

* **Knob**: A versatile rotary knob for parameter control with multiple visual variants (abstract, simplest, plainCap, iconCap)
* **Slider**: A linear slider for horizontal or vertical adjustments with multiple visual variants (abstract, trackless, trackfull, stripless)
* **Button**: A customizable button with styles suited for audio interfaces, supporting both toggle and momentary modes. Additional variants coming in future releases.
* **CycleButton**: A discrete control for cycling through a set of options with multiple visual variants. Additional variants coming in future releases.

### Device Components

* **Keys**: A responsive and interactive piano keyboard component

### Raster Components

* **FilmStrip Controls**: Bitmap sprite sheet-based controls for industry-standard control representation
* **Image-Based Controls**: Image-based knobs and switches for custom visual designs

### Primitives

* **Control Primitives**: Low-level control components for building custom controls
* **SVG Primitives**: SVG view primitives for composing custom radial and linear controls

## Architecture

AudioUI is organized as a monorepo with a clear separation between framework-agnostic core logic and framework-specific implementations:

* **`@cutoff/audio-ui-core`**: Framework-agnostic package containing all business logic, models, controllers, utilities, and styles. Can be used by any framework implementation or in non-framework contexts.

* **`@cutoff/audio-ui-react`**: The React component library implementation, published to npm. Provides React components, hooks, and React-specific adapters that wrap the framework-agnostic core.

This architecture enables potential implementations for other frameworks (Solid, Vue, etc.) that would share the same core logic.

## Resources

* **Playground**: Try AudioUI components live, explore examples, and see customization patterns at [playground.cutoff.dev](https://playground.cutoff.dev). The playground source code is available and contains multiple examples, including a dedicated [customization page](https://playground.cutoff.dev/examples/customization) with totally customized components built with AudioUI primitives.
* **Community**: Join our [Discord server](https://discord.gg/7RB6t2xqYW) for discussions, general questions, and support. Discussions are preferred in Discord channels (general, support, etc.).
* **Support**: Create support issues in the [GitHub repository Issues](https://github.com/cutoff/audio-ui/issues)
* **Wishlist**: A dedicated wishlist channel is available in the Cutoff Discord, allowing you to create feature requests and vote for the most wanted features
* **Repository**: [GitHub](https://github.com/cutoff/audio-ui)

## Next Steps

Ready to get started? Check out the [Installation](https://cutoff.dev/audio-ui/docs/latest/getting-started/installation) guide to set up AudioUI in your project, or jump to the [Quick Start Guide](https://cutoff.dev/audio-ui/docs/latest/getting-started/quick-start-guide) for a hands-on introduction.


---

# Installation

# Installation

> Install AudioUI in your project. Choose your framework for framework-specific setup instructions.

This guide covers the general installation process for AudioUI. For framework-specific setup instructions, see the framework installation pages linked below.

## Prerequisites

Before installing AudioUI, ensure you have:

* **Node.js** 18.0 or higher
* **React** 18.2.0 or higher (peer dependency)
* A package manager: `pnpm`, `npm`, `yarn`, or `bun`

## Installation Steps

### Install Peer Dependencies

AudioUI requires React and React DOM as peer dependencies. Install them first:

#### pnpm

```bash
pnpm add react@^19.0.0 react-dom@^19.0.0
```

#### npm

```bash
npm install react@^19.0.0 react-dom@^19.0.0
```

#### yarn

```bash
yarn add react@^19.0.0 react-dom@^19.0.0
```

#### bun

```bash
bun add react@^19.0.0 react-dom@^19.0.0
```

### Install the Package

Install AudioUI using your preferred package manager:

#### pnpm

```bash
pnpm add @cutoff/audio-ui-react
```

#### npm

```bash
npm install @cutoff/audio-ui-react
```

#### yarn

```bash
yarn add @cutoff/audio-ui-react
```

#### bun

```bash
bun add @cutoff/audio-ui-react
```

### Import the CSS

Import the AudioUI stylesheet in your application's entry point (e.g., `main.tsx`, `App.tsx`, or `_app.tsx`):

```tsx
import "@cutoff/audio-ui-react/style.css";
```

This import must be included for components to render correctly.

### Verify Installation

Create a simple test component to verify everything is working:

```tsx
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";

export default function TestComponent() {
  const [value, setValue] = useState(0.5);
  return <Knob value={value} onChange={(e) => setValue(e.value)} label="Test" />;
}
```

If the component renders without errors, installation is successful.

## Framework-Specific Installation

For detailed setup instructions tailored to your framework, see:

* [Vite](https://cutoff.dev/audio-ui/docs/latest/getting-started/installation/vite) - Fast build tool and dev server
* [Next.js](https://cutoff.dev/audio-ui/docs/latest/getting-started/installation/nextjs) - React framework for production
* [Tauri](https://cutoff.dev/audio-ui/docs/latest/getting-started/installation/tauri) - Build desktop applications with web technologies - Build desktop applications with web technologies

## Package Information

* **Package name**: `@cutoff/audio-ui-react`
* **Current version**: `latest` (Developer Preview)
* **License**: GPL-3.0-only (open source) or Commercial license available

## Next Steps

Once installation is complete, check out the [Quick Start Guide](https://cutoff.dev/audio-ui/docs/latest/getting-started/quick-start-guide) to learn how to use AudioUI components in your application.


---

# Vite

# Vite

> Install and set up AudioUI in a Vite project. Vite provides fast development and optimized production builds.

This guide covers installing AudioUI in a Vite project. Vite is a fast build tool and development server that's ideal for React applications.

## Prerequisites

* Node.js 18.0 or higher
* Basic familiarity with React and Vite

## Installation Steps

### Create a Vite Project

If you don't have a Vite project yet, create one:

#### pnpm

```bash
pnpm create vite my-audio-app --template react-ts
cd my-audio-app
pnpm install
```

#### npm

```bash
npm create vite@latest my-audio-app -- --template react-ts
cd my-audio-app
npm install
```

#### yarn

```bash
yarn create vite my-audio-app --template react-ts
cd my-audio-app
yarn install
```

#### bun

```bash
bun create vite my-audio-app --template react-ts
cd my-audio-app
bun install
```

This creates a new Vite project with React and TypeScript.

### Install AudioUI

Install AudioUI using your package manager:

#### pnpm

```bash
pnpm add @cutoff/audio-ui-react
```

#### npm

```bash
npm install @cutoff/audio-ui-react
```

#### yarn

```bash
yarn add @cutoff/audio-ui-react
```

#### bun

```bash
bun add @cutoff/audio-ui-react
```

### Import CSS

Import the AudioUI stylesheet in your css file. In a Vite project, this is typically `src/index.css`:

```tsx title="src/index.css" showLineNumbers
@import url("@cutoff/audio-ui-react/style.css");

// Rest of the styles...
```

### Create Your First Component

Create a simple component to test AudioUI:

```tsx title="src/App.tsx" showLineNumbers
import { useState } from "react";
import './App.css'
import { Knob } from "@cutoff/audio-ui-react";

export default function App() {
    const [value, setValue] = useState(0.5);

    return (
        <Knob value={value}
              onChange={(e) => setValue(e.value)}
              label="Cutoff"
              size="large"
        />
    );
}
```

### Start Development Server

Start the Vite development server:

#### pnpm

```bash
pnpm dev
```

#### npm

```bash
npm dev
```

#### yarn

```bash
yarn dev
```

#### bun

```bash
bun run dev
```

Open your browser to the URL shown in the terminal (typically `http://localhost:5173`). You should see your AudioUI component rendering.

## Configuration

Vite works with AudioUI out of the box. No additional configuration is required. The CSS import is handled automatically by Vite's CSS processing.

## Building for Production

Build your application for production:

### pnpm

```bash
pnpm build
```

### npm

```bash
npm build
```

### yarn

```bash
yarn build
```

### bun

```bash
bun run build
```

The optimized build will be in the `dist` directory. AudioUI styles are automatically included in the build.

## Next Steps

* Check out the [Quick Start Guide](https://cutoff.dev/audio-ui/docs/latest/getting-started/quick-start-guide) to learn more about using AudioUI components
* Explore [Code Examples](https://cutoff.dev/audio-ui/docs/latest/getting-started/code-examples) for practical usage patterns
* Read the [Component Documentation](https://cutoff.dev/audio-ui/docs/latest/components) for detailed API references


---

# Next.js

# Next.js

> Install and set up AudioUI in a Next.js project using the App Router.

This guide covers installing AudioUI in a Next.js project using the App Router (Next.js 13+). Next.js is a React framework optimized for production.

## Prerequisites

* Node.js 18.0 or higher
* Next.js 13.0 or higher (App Router)
* Basic familiarity with React and Next.js

## Installation Steps

### Create a Next.js Project

If you don't have a Next.js project yet, create one:

#### pnpm

```bash
pnpm create next-app@latest my-audio-app
cd my-audio-app
```

#### npm

```bash
npx create-next-app@latest my-audio-app
cd my-audio-app
```

#### yarn

```bash
yarn create next-app@latest my-audio-app
cd my-audio-app
```

#### bun

```bash
bunx create-next-app@latest my-audio-app
cd my-audio-app
```

When prompted, choose:

* TypeScript: Yes
* App Router: Yes
* Other options as needed

### Install AudioUI

Install AudioUI using your package manager:

#### pnpm

```bash
pnpm add @cutoff/audio-ui-react
```

#### npm

```bash
npm install @cutoff/audio-ui-react
```

#### yarn

```bash
yarn add @cutoff/audio-ui-react
```

#### bun

```bash
bun add @cutoff/audio-ui-react
```

### Import CSS

Import the AudioUI stylesheet in your root stylsheet. In Next.js, this is `app/globals.css`:

```tsx title="app/globals.css" showLineNumbers
@import url("@cutoff/audio-ui-react/style.css");

// Rest of your styles...
```

### Create a Client Component

AudioUI components are client components. Create a client component for your audio controls:

```tsx title="components/AudioControls.tsx" showLineNumbers
"use client";

import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";

export default function AudioControls() {
  const [value, setValue] = useState(0.5);

  return (
    <Knob
        value={value}
        onChange={(e) => setValue(e.value)}
        label="Cutoff"
        size="large"
      />
  );
}
```

### Use in a Page

Use your component in a page:

```tsx title="app/page.tsx" showLineNumbers
import AudioControls from "../components/AudioControls";

export default function Home() {
    return (
        <main className="flex flex-col items-center">
            <h1>My Audio App</h1>
            <AudioControls />
        </main>
    );
}
```

### Start Development Server

Start the Next.js development server:

#### pnpm

```bash
pnpm run dev
```

#### npm

```bash
npm run dev
```

#### yarn

```bash
yarn dev
```

#### bun

```bash
bun run dev
```

Open your browser to `http://localhost:3000`. You should see your AudioUI component rendering.

## Important Notes

### Client Components

All AudioUI components must be used in client components. Always include the `"use client"` directive at the top of files that use AudioUI components.

### Server Components

If you need to use AudioUI components in a server component, create a separate client component wrapper:

```tsx title="app/components/AudioPanel.tsx" showLineNumbers
"use client";

import { Knob, Slider } from "@cutoff/audio-ui-react";
// ... component implementation
```

Then import and use it in your server component.

## Building for Production

Build your application for production:

### pnpm

```bash
pnpm run build
```

### npm

```bash
npm run build
```

### yarn

```bash
yarn build
```

### bun

```bash
bun run build
```

AudioUI styles are automatically included in the Next.js build output.

## Next Steps

* Check out the [Quick Start Guide](https://cutoff.dev/audio-ui/docs/latest/getting-started/quick-start-guide) to learn more about using AudioUI components
* Explore [Code Examples](https://cutoff.dev/audio-ui/docs/latest/getting-started/code-examples) for practical usage patterns
* Read the [Component Documentation](https://cutoff.dev/audio-ui/docs/latest/components) for detailed API references


---

# Tauri

# Tauri

> Install and set up AudioUI in a Tauri application for building desktop audio applications.

This guide covers installing AudioUI in a Tauri application. Tauri allows you to build desktop applications using web technologies, making it ideal for audio applications that need native desktop performance.

## Prerequisites

* Node.js 18.0 or higher
* Rust (for Tauri backend)
* Basic familiarity with React and Tauri

## Installation Steps

### Create a Tauri Project

If you don't have a Tauri project yet, create one. You can use either interactive or non-interactive setup:

**Interactive setup:**

#### pnpm

```bash
pnpm create tauri-app@latest
```

#### npm

```bash
npm create tauri-app@latest
```

#### yarn

```bash
yarn create tauri-app
```

#### bun

```bash
bun create tauri-app
```

When prompted:

* Choose your package manager (npm, pnpm, yarn, bun)
* Choose "React" as the UI framework
* Choose "TypeScript" if you want TypeScript support

**Non-interactive setup:**

You can skip prompts by using CLI flags:

#### pnpm

```bash
npx create-tauri-app@latest tauri-project \
    --manager pnpm \
    --template react-ts \
    --yes
```

#### npm

```bash
npx create-tauri-app@latest tauri-project \
    --manager npm \
    --template react-ts \
    --yes
```

#### yarn

```bash
npx create-tauri-app@latest tauri-project \
    --manager yarn \
    --template react-ts \
    --yes
```

#### bun

```bash
npx create-tauri-app@latest tauri-project \
    --manager bun \
    --template react-ts \
    --yes
```

This creates a new Tauri project with React and TypeScript.

### Install AudioUI

Navigate to your project directory and install AudioUI:

#### pnpm

```bash
cd my-tauri-app
pnpm add @cutoff/audio-ui-react
```

#### npm

```bash
cd my-tauri-app
npm install @cutoff/audio-ui-react
```

#### yarn

```bash
cd my-tauri-app
yarn add @cutoff/audio-ui-react
```

#### bun

```bash
cd my-tauri-app
bun add @cutoff/audio-ui-react
```

### Import CSS

Import the AudioUI stylesheet in your main css file. In a Tauri React project, this is typically `src/App.css`:

```tsx title="src/App.css" showLineNumber
@import url("@cutoff/audio-ui-react/style.css");
// Rest of the styles
```

### Create Your First Component

Create a simple component to test AudioUI:

```tsx title="src/App.tsx" showLineNumbers
import { useState } from "react";
import "./App.css";
import { Knob } from "@cutoff/audio-ui-react";

export default function App() {
  const [value, setValue] = useState(0.5);
  return (
    <main className="container">
      <h1>My Tauri Audio App</h1>
      <div className="row">
        <Knob
          value={value}
          onChange={(e) => setValue(e.value)}
          label="Cutoff"
          size="large"
        />
      </div>
    </main>
  );
}
```

### Start Development

Start the Tauri development server:

#### pnpm

```bash
pnpm tauri dev
```

#### npm

```bash
npm tauri dev
```

#### yarn

```bash
yarn tauri dev
```

#### bun

```bash
bun run tauri dev
```

This will:

1. Start the Vite dev server for the frontend
2. Compile the Rust backend
3. Open a native window with your application

You should see your AudioUI component rendering in the Tauri window.

## Building for Production

Build your Tauri application for production:

### pnpm

```bash
pnpm tauri build
```

### npm

```bash
npm tauri build
```

### yarn

```bash
yarn tauri build
```

### bun

```bash
bun run tauri build
```

This creates platform-specific installers (`.dmg` on macOS, `.exe` on Windows, `.AppImage` on Linux) in `src-tauri/target/release/bundle/`.

## Next Steps

* Check out the [Quick Start Guide](https://cutoff.dev/audio-ui/docs/latest/getting-started/quick-start-guide) to learn more about using AudioUI components
* Explore [Code Examples](https://cutoff.dev/audio-ui/docs/latest/getting-started/code-examples) for practical usage patterns
* Read the [Component Documentation](https://cutoff.dev/audio-ui/docs/latest/components) for detailed API references
* Learn about [Tauri commands](https://tauri.app/v1/guides/features/command) for native functionality


---

# Quick Start Guide

# Quick Start Guide

> Get up and running with AudioUI in minutes. Learn the basics of using AudioUI components in your application.

This guide will help you get started with AudioUI by building a simple audio control interface. You'll learn the basic patterns for using AudioUI components.

## Minimal Setup

First, ensure AudioUI is installed and the CSS is imported:

```tsx title="App.tsx" showLineNumbers
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";
import "@cutoff/audio-ui-react/style.css";

export default function App() {
  const [cutoff, setCutoff] = useState(0.5);

  return (
    <div>
      <Knob
        value={cutoff}
        onChange={(e) => setCutoff(e.value)}
        label="Cutoff"
      />
    </div>
  );
}
```

This creates a basic knob control. The `value` prop controls the current value, and `onChange` is called when the user interacts with the knob.

## Basic Usage Pattern

AudioUI components follow a controlled component pattern. You manage the state in your component and pass it to AudioUI:

```tsx title="AudioControls.tsx" showLineNumbers {1-25}
import { useState } from "react";
import { Knob, Slider } from "@cutoff/audio-ui-react";

export default function AudioControls() {
  const [cutoff, setCutoff] = useState(0.5);
  const [resonance, setResonance] = useState(0.3);
  const [volume, setVolume] = useState(0.7);

  return (
    <div style={{ display: "flex", gap: "20px", padding: "20px" }}>
      <Knob
        value={cutoff}
        onChange={(e) => setCutoff(e.value)}
        label="Cutoff"
      />
      <Knob
        value={resonance}
        onChange={(e) => setResonance(e.value)}
        label="Resonance"
      />
      <Slider
        value={volume}
        onChange={(e) => setVolume(e.value)}
        label="Volume"
        orientation="vertical"
      />
    </div>
  );
}
```

## Working with Value Ranges

AudioUI components accept real values. When you provide `min` and `max` props, the component handles the conversion internally:

```tsx title="ParameterControl.tsx" showLineNumbers {8-15}
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";

export default function ParameterControl() {
  const [frequency, setFrequency] = useState(1000);

  return (
    <Knob
      value={frequency}
      onChange={(e) => setFrequency(e.value)}
      label="Frequency"
      min={20}
      max={20000}
    />
  );
}
```

The `onChange` event provides the value in the range you specified. AudioUI handles the normalization internally.

## Using Buttons

Buttons support both toggle (latch) and momentary modes:

```tsx title="ButtonExample.tsx" showLineNumbers
import { useState } from "react";
import { Button } from "@cutoff/audio-ui-react";

export default function ButtonExample() {
  const [isOn, setIsOn] = useState(false);
  const [isPressed, setIsPressed] = useState(false);

  return (
    <div style={{ display: "flex", gap: "10px" }}>
      <Button
        label="Power"
        latch={true}
        value={isOn}
        onChange={(e) => setIsOn(e.value)}
      />
      <Button
        label="Record"
        latch={false}
        value={isPressed}
        onChange={(e) => setIsPressed(e.value)}
      />
    </div>
  );
}
```

## Combining Components

Here's a complete example combining multiple components:

```tsx title="SynthesizerPanel.tsx" showLineNumbers
import { useState } from "react";
import { Knob, Slider, Button } from "@cutoff/audio-ui-react";

export default function SynthesizerPanel() {
  const [cutoff, setCutoff] = useState(0.5);
  const [resonance, setResonance] = useState(0.3);
  const [volume, setVolume] = useState(0.7);
  const [isActive, setIsActive] = useState(true);

  return (
    <div style={{ display: "flex", gap: "20px", padding: "20px" }}>
      <Button
        label="Power"
        latch={true}
        value={isActive}
        onChange={(e) => setIsActive(e.value)}
      />
      <Knob
        value={cutoff}
        onChange={(e) => setCutoff(e.value)}
        label="Cutoff"
        min={20}
        max={20000}
      />
      <Knob
        value={resonance}
        onChange={(e) => setResonance(e.value)}
        label="Resonance"
        min={0}
        max={100}
      />
      <Slider
        value={volume}
        onChange={(e) => setVolume(e.value)}
        label="Volume"
        orientation="vertical"
        min={0}
        max={100}
      />
    </div>
  );
}
```

## Next Steps

Now that you understand the basics:

* Explore [Code Examples](https://cutoff.dev/audio-ui/docs/latest/getting-started/code-examples) for more component usage patterns
* Check out individual [Component Documentation](https://cutoff.dev/audio-ui/docs/latest/components) for detailed API references
* Learn about [theming and customization](https://cutoff.dev/audio-ui/docs/latest/getting-started/code-examples) to match your application's design


---

# Code Examples

# Code Examples

> Practical code examples for AudioUI components. Copy and paste these examples to get started quickly.

This page provides working code examples for AudioUI components. Each example is complete and ready to use in your project.

## Basic Components

### knob

```tsx title="KnobExample.tsx" showLineNumbers
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";

export default function KnobExample() {
  const [value, setValue] = useState(0.5);

  return (
    <Knob
      value={value}
      onChange={(e) => setValue(e.value)}
      label="Cutoff"
      min={20}
      max={20000}
    />
  );
}
```

### slider

```tsx title="SliderExample.tsx" showLineNumbers
import { useState } from "react";
import { Slider } from "@cutoff/audio-ui-react";

export default function SliderExample() {
  const [verticalValue, setVerticalValue] = useState(0.7);
  const [horizontalValue, setHorizontalValue] = useState(0.5);

  return (
    <div style={{ display: "flex", gap: "20px" }}>
      <Slider
        value={verticalValue}
        onChange={(e) => setVerticalValue(e.value)}
        label="Volume"
        orientation="vertical"
        min={0}
        max={100}
      />
      <Slider
        value={horizontalValue}
        onChange={(e) => setHorizontalValue(e.value)}
        label="Pan"
        orientation="horizontal"
        min={-100}
        max={100}
      />
    </div>
  );
}
```

### button

```tsx title="ButtonExample.tsx" showLineNumbers
import { useState } from "react";
import { Button } from "@cutoff/audio-ui-react";

export default function ButtonExample() {
  const [isOn, setIsOn] = useState(false);
  const [isPressed, setIsPressed] = useState(false);

  return (
    <div style={{ display: "flex", gap: "10px" }}>
      <Button
        label="Power"
        latch={true}
        value={isOn}
        onChange={(e) => setIsOn(e.value)}
      />
      <Button
        label="Record"
        latch={false}
        value={isPressed}
        onChange={(e) => setIsPressed(e.value)}
      />
    </div>
  );
}
```

### cycle

```tsx title="CycleButtonExample.tsx" showLineNumbers
import { useState } from "react";
import { CycleButton, OptionView } from "@cutoff/audio-ui-react";

export default function CycleButtonExample() {
  const [waveform, setWaveform] = useState("sine");

  return (
    <CycleButton
      value={waveform}
      onChange={(e) => setWaveform(e.value)}
      label="Waveform"
    >
        <OptionView value="sine">Sine</OptionView>
        <OptionView value="square">Square</OptionView>
        <OptionView value="sawtooth">Saw</OptionView>
        <OptionView value="triangle">Tri</OptionView>
    </CycleButton>
  );
}
```

## Keys Component

The Keys component provides an interactive piano keyboard:

```tsx title="KeysExample.tsx" showLineNumbers
import { useState } from "react";
import { Keys } from "@cutoff/audio-ui-react";

export default function KeysExample() {
  const [notesOn, setNotesOn] = useState<(string | number)[]>([]);

  const handleChange = (e: { value: { note: number; active: boolean } }) => {
    const noteNum = e.value.note;
    if (e.value.active) {
      setNotesOn((prev) => [...prev, noteNum]);
      console.log("Note on:", noteNum);
    } else {
      setNotesOn((prev) => prev.filter((n) => n !== noteNum));
      console.log("Note off:", noteNum);
    }
  };

  return (
    <Keys
      nbKeys={61}
      notesOn={notesOn}
      onChange={handleChange}
    />
  );
}
```

## Combining Multiple Components

Here's a complete synthesizer panel example:

```tsx title="SynthesizerPanel.tsx" showLineNumbers
import { useState } from "react";
import {
  Knob,
  Slider,
  Button,
  CycleButton,
  OptionView,
} from "@cutoff/audio-ui-react";

export default function SynthesizerPanel() {
  const [cutoff, setCutoff] = useState(0.5);
  const [resonance, setResonance] = useState(0.3);
  const [volume, setVolume] = useState(0.7);
  const [isActive, setIsActive] = useState(true);
  const [waveform, setWaveform] = useState("sine");

  return (
    <div style={{ 
      display: "flex", 
      gap: "20px", 
      padding: "20px",
      flexWrap: "wrap"
    }}>
      <Button
        label="Power"
        latch={true}
        value={isActive}
        onChange={(e) => setIsActive(e.value)}
      />
      <CycleButton
        value={waveform}
        onChange={(e) => setWaveform(e.value)}
        label="Waveform"
      >
        <OptionView value="sine">Sine</OptionView>
        <OptionView value="square">Square</OptionView>
        <OptionView value="sawtooth">Saw</OptionView>
      </CycleButton>
      <Knob
        value={cutoff}
        onChange={(e) => setCutoff(e.value)}
        label="Cutoff"
        min={20}
        max={20000}
      />
      <Knob
        value={resonance}
        onChange={(e) => setResonance(e.value)}
        label="Resonance"
        min={0}
        max={100}
      />
      <Slider
        value={volume}
        onChange={(e) => setVolume(e.value)}
        label="Volume"
        orientation="vertical"
        min={0}
        max={100}
      />
    </div>
  );
}
```

## Using Value Formatters

AudioUI provides formatters for common audio parameter displays:

```tsx title="FormatterExample.tsx" showLineNumbers
import { useState } from "react";
import {
  Knob,
  frequencyFormatter,
  percentageFormatter,
} from "@cutoff/audio-ui-react";

export default function FormatterExample() {
  const [frequency, setFrequency] = useState(1000);
  const [mix, setMix] = useState(50);

  return (
    <div style={{ display: "flex", gap: "20px" }}>
      <Knob
        value={frequency}
        onChange={(e) => setFrequency(e.value)}
        label="Frequency"
        min={20}
        max={20000}
        valueFormatter={(value) => frequencyFormatter(value)}
      />
      <Knob
        value={mix}
        onChange={(e) => setMix(e.value)}
        label="Mix"
        min={0}
        max={100}
        valueFormatter={(value, paramDef) => {
          return percentageFormatter(value, paramDef.min, paramDef.max);
        }}
      />
    </div>
  );
}
```

## Theming

AudioUI supports theming through the ThemeManager. You can customize the primary color and roundness attribute:

```tsx title="ThemedExample.tsx" showLineNumbers
import { useState, useEffect } from "react";
import { Knob, setThemeColor, setThemeRoundness } from "@cutoff/audio-ui-react";

export default function ThemedExample() {
  const [value, setValue] = useState(0.5);

  // Set theme (runs once, affects all components)
  useEffect(() => {
    setThemeColor("#3b82f6"); // Blue accent
    setThemeRoundness(0.3); // Slightly rounded
  }, []);

  return (
    <Knob
      value={value}
      onChange={(e) => setValue(e.value)}
      label="Themed Knob"
    />
  );
}
```

The theme system currently supports primary color and roundness. For an overview of theming on vector components, see [Theming Features](https://cutoff.dev/audio-ui/docs/latest/components/vector/introduction#theming-features) in the Vector introduction. Additional theme options (glowness, reflectiveness, and more) are planned for the 1.0 release.

## Dark and Light Mode

AudioUI components automatically adapt to dark and light themes based on your application's theme system. The components use CSS variables that respond to the `prefers-color-scheme` media query or your theme provider.

To control the theme in your example apps:

### Using CSS

You can control the theme by setting the `color-scheme` CSS property or using a class:

```tsx title="App.tsx" showLineNumbers
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";
import "@cutoff/audio-ui-react/style.css";

export default function App() {
  const [value, setValue] = useState(0.5);
  const [isDark, setIsDark] = useState(false);

  return (
    <div style={{ colorScheme: isDark ? "dark" : "light" }}>
      <button onClick={() => setIsDark(!isDark)}>
        Toggle Theme
      </button>
      <Knob
        value={value}
        onChange={(e) => setValue(e.value)}
        label="Cutoff"
      />
    </div>
  );
}
```

### Using a Theme Provider

If you're using a theme provider (like `next-themes` or a custom solution), AudioUI components will automatically adapt:

```tsx title="App.tsx" showLineNumbers
import { useState } from "react";
import { useTheme } from "next-themes";
import { Knob } from "@cutoff/audio-ui-react";
import "@cutoff/audio-ui-react/style.css";

export default function App() {
  const { theme, setTheme } = useTheme();
  const [value, setValue] = useState(0.5);

  return (
    <div>
      <button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
        Toggle Theme
      </button>
      <Knob
        value={value}
        onChange={(e) => setValue(e.value)}
        label="Cutoff"
      />
    </div>
  );
}
```

The components will automatically update their appearance when the theme changes, ensuring proper contrast and visibility in both light and dark modes.

## Component Variants

AudioUI components support multiple visual variants. Here are examples using different variants:

```tsx title="VariantsExample.tsx" showLineNumbers
import { useState } from "react";
import { Knob, Slider } from "@cutoff/audio-ui-react";

export default function VariantsExample() {
  const [value1, setValue1] = useState(0.5);
  const [value2, setValue2] = useState(0.5);
  const [value3, setValue3] = useState(0.5);

  return (
    <div style={{ display: "flex", gap: "20px" }}>
      <Knob
        value={value1}
        onChange={(e) => setValue1(e.value)}
        label="Abstract"
        variant="abstract"
      />
      <Knob
        value={value2}
        onChange={(e) => setValue2(e.value)}
        label="PlainCap"
        variant="plainCap"
      />
      <Slider
        value={value3}
        onChange={(e) => setValue3(e.value)}
        label="Trackless"
        variant="trackless"
        orientation="vertical"
      />
    </div>
  );
}
```

Knob supports variants: `abstract`, `simplest`, `plainCap`, `iconCap`. Slider supports variants: `abstract`, `trackless`, `trackfull`, `stripless`. Additional variants for CycleButton and Button are coming in future releases.

## Next Steps

* Explore individual [Component Documentation](https://cutoff.dev/audio-ui/docs/latest/components) for detailed API references
* Learn about [advanced patterns](https://cutoff.dev/audio-ui/docs/latest/getting-started/quick-start-guide) and customization options
* Check out the [FAQ](https://cutoff.dev/audio-ui/docs/latest/getting-started/faq) for common questions


---

# Components

# Components

> An overview of AudioUI components—what they are, how they are organized, and how to choose the right type for your project.

AudioUI provides ready-to-use controls (knobs, sliders, buttons, cycle buttons, piano keys), bitmap-based controls that use your own assets (film strips and image knobs/switches), and primitives for fully custom controls. For design philosophy and performance priorities, see the [Introduction](https://cutoff.dev/audio-ui/docs/latest/getting-started/introduction). For more context on each type before diving into individual components, see the [Vector introduction](https://cutoff.dev/audio-ui/docs/latest/components/vector/introduction) and [Raster introduction](https://cutoff.dev/audio-ui/docs/latest/components/raster/introduction).

Components fall into four types: **Vector Components**, **Raster Components**, **Control Primitives**, and **View Primitives**. The table below compares these types on several criteria so you can see how they differ and choose the right one.

## Comparing the four component types

Each column is one component type and links to its documentation. The rows are comparison criteria (attributes):

* **Opinionated**: Fixed look-and-feel choices vs full control over appearance (you supply or compose the view).
* **Themable (built-in)**: Whether the component type supports the built-in theme system (ThemeManager, color, roundness).
* **Resolution-independent scaling**: Whether the visual scales without pixelation (SVG) or may pixelate when scaled (bitmap). View primitives are mostly resolution-independent but not always (e.g. bitmap used in a compound view).
* **Default view**: Whether the component type provides ready-to-use visuals without supplying assets.
* **Optimization responsibility**: Who is responsible for performance and optimization. For view primitives: user for compound views; the primitives themselves are framework-optimized.
* **Dark/light variants**: Built-in theme support for light and dark mode; or (raster) props to supply separate dark and light assets with transition managed by the component; or user responsibility / N/A.
* **Completeness**: Whether the component type is considered complete or will receive new components, variants, or primitives over time.

\| Attribute | [Vector Components](https://cutoff.dev/audio-ui/docs/latest/components/vector/introduction) | [Raster Components](https://cutoff.dev/audio-ui/docs/latest/components/raster/introduction) | [Control Primitives](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/control-primitives) | [View Primitives](https://cutoff.dev/audio-ui/docs/latest/advanced-customization/view-system-view-primitives) |
\| --------- | ----------------- | -------------------- | ------------------ | ------------------- |
\| Opinionated | Yes | No | No | No |
\| Themable (built-in) | Yes | No | No | No |
\| Resolution-independent scaling | Yes | No (may pixelate) | Depends on view | Mostly |
\| Default view | Yes | No (requires assets) | No | No |
\| Optimization responsibility | Framework | Framework | User | User (compound views); primitives framework-optimized |
\| Dark/light variants | Built-in | Yes (dark/light assets) | N/A | User responsibility |
\| Completeness | Will expand (components, variants, theming) | Complete | Complete (parameter model complete) | May expand (new primitives) |

* **Vector Components** (Knob, Slider, Button, CycleButton, Keys) are opinionated: they aim to cover 90% of needs with fixed look-and-feel choices; not everything is customizable. They are themable, scale without pixelation, and have a default view.
* **Raster Components** (film strip and image-based controls) are unopinionated: you supply the assets, so any visual representation is possible. They do not use the built-in theme system and may pixelate when scaled.
* **Control Primitives** and **View Primitives** are non-opinionated building blocks: you compose a custom view and are responsible for styling and optimization. View primitives include vector primitives (SVG fragments) and may include other building blocks; use them when you need full control over appearance.

## Next steps

* Use [Vector Components](https://cutoff.dev/audio-ui/docs/latest/components/vector/introduction) (e.g. [Knob](https://cutoff.dev/audio-ui/docs/latest/components/vector/knob), [Slider](https://cutoff.dev/audio-ui/docs/latest/components/vector/slider)) for ready-to-use, themable controls and [Theming Features](https://cutoff.dev/audio-ui/docs/latest/components/vector/introduction#theming-features).
* Use [Raster Components](https://cutoff.dev/audio-ui/docs/latest/components/raster/introduction) when you have bitmap assets (sprite sheets or images) and want pixel-accurate or custom visuals.
* Use [Control Primitives](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/control-primitives) and [View Primitives](https://cutoff.dev/audio-ui/docs/latest/advanced-customization/view-system-view-primitives) to build fully custom controls.


---

# FAQ

# FAQ

> Frequently asked questions about AudioUI, installation, usage, and support.

## General Questions

### What is AudioUI?

AudioUI is a React component library for building user interfaces in audio and MIDI applications. For an overview of what it provides and its design philosophy, see the [Introduction](https://cutoff.dev/audio-ui/docs/latest/getting-started/introduction).

### Is AudioUI production-ready?

AudioUI is in **Developer Preview**; pin to specific versions for production use. For current status, in-progress work, and 1.0 plans, see [Introduction – Project Status](https://cutoff.dev/audio-ui/docs/latest/getting-started/introduction#project-status).

### What React version is required?

AudioUI requires React 18.2.0 or higher. This is specified as a peer dependency, so you'll need to ensure your project uses a compatible React version. AudioUI supports both React 18 and React 19 without any compatibility issues.

### What license does AudioUI use?

AudioUI is dual-licensed:

* **GPL-3.0-only**: Free for open-source projects
* **Commercial License**: Required for proprietary, closed-source applications. Available at [cutoff.dev](https://cutoff.dev)

## Installation and Setup

### How do I install AudioUI?

Install the package using your package manager:

### pnpm

```bash
pnpm add @cutoff/audio-ui-react
```

### npm

```bash
npm install @cutoff/audio-ui-react
```

### yarn

```bash
yarn add @cutoff/audio-ui-react
```

### bun

```bash
bun add @cutoff/audio-ui-react
```

Don't forget to import the CSS:

```tsx
import "@cutoff/audio-ui-react/style.css";
```

See the [Installation Guide](https://cutoff.dev/audio-ui/docs/latest/getting-started/installation) for detailed instructions.

### Do I need to install peer dependencies?

Yes. AudioUI requires React and React DOM as peer dependencies. If they're not already in your project, install them:

### pnpm

```bash
pnpm add react@^19.0.0 react-dom@^19.0.0
```

### npm

```bash
npm install react@^19.0.0 react-dom@^19.0.0
```

### yarn

```bash
yarn add react@^19.0.0 react-dom@^19.0.0
```

### bun

```bash
bun add react@^19.0.0 react-dom@^19.0.0
```

### Which frameworks are supported?

AudioUI is a React library, so it works with any React-based framework. We provide detailed installation guides for:

* Vite
* Next.js
* Tauri

See the [Installation](https://cutoff.dev/audio-ui/docs/latest/getting-started/installation) section for framework-specific guides.

## Usage

### How do I customize the appearance?

AudioUI provides a ThemeManager for theming. You can customize the primary color and roundness attribute:

```tsx
import { setThemeColor, setThemeRoundness } from "@cutoff/audio-ui-react";

setThemeColor("#3b82f6");
setThemeRoundness(0.3);
```

The theme system currently supports primary color and roundness. For an overview of theming on vector components, see [Theming Features](https://cutoff.dev/audio-ui/docs/latest/components/vector/introduction#theming-features) in the Vector introduction. Additional theme options (glowness, reflectiveness, and more) are planned for the 1.0 release.

Components also support multiple visual variants. For example, Knob has variants like `abstract`, `simplest`, `plainCap`, and `iconCap`, while Slider has variants like `abstract`, `trackless`, `trackfull`, and `stripless`. You can fine-tune components using styles and props.

For advanced customization, you can use the non-opinionated primitives to build completely custom components. The [customization page](https://playground.cutoff.dev/examples/customization) contains multiple examples of totally customized components, and its source code is available for reference.

### How do I handle parameter changes?

AudioUI components use a controlled component pattern. You manage state in your component and pass it via props:

```tsx
const [value, setValue] = useState(0.5);

<Knob
  value={value}
  onChange={(e) => setValue(e.value)}
  label="Cutoff"
/>
```

The `onChange` event provides the new value in the `e.value` property.

### Can I use AudioUI with other frameworks?

The core package (`@cutoff/audio-ui-core`) is framework-agnostic and contains all business logic. Currently, only the React implementation (`@cutoff/audio-ui-react`) is available, but the architecture supports potential implementations for other frameworks like Solid or Vue.

## Technical Questions

### Does AudioUI support mobile devices?

Yes. AudioUI components are responsive and touch-optimized, making them suitable for mobile applications.

### How do I use AudioUI with TypeScript?

AudioUI is written entirely in TypeScript and provides full type definitions. No additional setup is required when using TypeScript.

### Can I use AudioUI in a server-side rendered (SSR) application?

Yes, but you'll need to ensure components are only rendered on the client side. In Next.js, use the `"use client"` directive or render components conditionally. AudioUI components are client components by default.

## Support and Community

### Where can I get help?

Playground, Discord, and GitHub — see [Introduction – Resources](https://cutoff.dev/audio-ui/docs/latest/getting-started/introduction#resources).

### How do I report a bug?

Open an issue on the [GitHub repository](https://github.com/cutoff/audio-ui/issues) with:

* Description of the issue
* Steps to reproduce
* Expected vs actual behavior
* Your environment (React version, browser, etc.)

### Can I contribute to AudioUI?

Yes! AudioUI is open source. Contributions are welcome. Check the repository for contribution guidelines and open issues labeled "good first issue" to get started.

### When will AudioUI reach version 1.0?

We're actively working toward a stable 1.0 release. For planned additions and timeline, see [Introduction – Project Status](https://cutoff.dev/audio-ui/docs/latest/getting-started/introduction#project-status). Join our Discord to stay updated on progress and share your feedback.

### How can I request features or vote on upcoming features?

A dedicated wishlist channel is available in the [Cutoff Discord server](https://discord.gg/7RB6t2xqYW). You can create feature requests and vote for the most wanted features. Community feedback helps prioritize development efforts.


---

# Introduction

# Vector Components

> Overview of AudioUI's built-in vector components—what they share (layout, parameters, interaction, theming) and how they differ (Knob, Slider, Button, CycleButton, Keys).

Vector components are the built-in, opinionated controls: [Knob](https://cutoff.dev/audio-ui/docs/latest/components/vector/knob), [Slider](https://cutoff.dev/audio-ui/docs/latest/components/vector/slider), [Button](https://cutoff.dev/audio-ui/docs/latest/components/vector/button), [CycleButton](https://cutoff.dev/audio-ui/docs/latest/components/vector/cycle-button), and [Keys](https://cutoff.dev/audio-ui/docs/latest/components/vector/keys). They are not just "vector vs raster": their design comes from a careful analysis of existing audio plugins and apps from many providers, and gathers the visual characteristics and variants found most often in real-world interfaces. The end-goal is to cover about 90% of the needs of most apps and plugins; the remaining 10% (advanced customization) is left to [control primitives](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/control-primitives) and [vector primitives](https://cutoff.dev/audio-ui/docs/latest/advanced-customization/view-system-view-primitives)—totally flexible, but more demanding and requiring care about performance and optimization. Built-in components are designed to optimize performance as far as possible. For how they fit with raster and primitives, see the [Components overview](https://cutoff.dev/audio-ui/docs/latest/getting-started/components) under Getting Started.

## What all vector components share

Every vector component provides:

* **Layout system**: AdaptiveBox with sizing modes (fixed sizes or adaptive), label positioning, and alignment. Labels can sit above or below, with configurable overflow and height.
* **Parameter model**: Support for ad-hoc props (`min`, `max`, `label`, etc.) or the full [Audio Parameter](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/audio-parameters) model (Continuous, Discrete, or Boolean) for scaling, units, formatting, and MIDI.
* **Interaction system**: Drag, wheel, and keyboard input with configurable sensitivity and modes. High-frequency state during interaction is handled so the rest of the tree does not re-render on every move.
* **Accessibility**: ARIA attributes, focus management, and keyboard navigation.
* **Theming**: Built-in support for primary color and roundness via ThemeManager or per-component props (`color`, `roundness`). Dark and light mode work out of the box.
* **Visual variants**: Multiple variants per component (e.g. Knob: abstract, simplest, plainCap, iconCap; Slider: abstract, trackless, trackfull, stripless) so you can match different plugin or app styles.

Vector components are SVG-based and scale without pixelation. They are the right choice when you want ready-to-use, themable controls without supplying your own assets. Details are in [Theming Features](#theming-features) below.

## Theming Features

* **color**: Primary (accent) color for the component. Any valid CSS color. Can be set per component via the `color` prop or globally via ThemeManager (`setThemeColor`). Used for value indicators, active states, and accents. Dark and light mode work out of the box.
* **roundness**: Controls corner/edge roundness, normalized 0.0–1.0. Set per component with the `roundness` prop or globally via `setThemeRoundness`. Affects tracks, caps, and buttons.
* **Future theme attributes**: The theme system will expand; attributes such as glowness and reflectiveness are planned (tentative for 1.0.0). See [Introduction – Project Status](https://cutoff.dev/audio-ui/docs/latest/getting-started/introduction#project-status) for roadmap.
* **Variants**: Most components have or will get multiple **variants** that change their aspect (e.g. Knob: abstract, simplest, plainCap, iconCap; Slider: abstract, trackless, trackfull, stripless). Use the `variant` prop; see each component's page for options.
* **Scale markers**: Knob- and slider-style components have or will get options for **scale markers** (graduations and indices) so you can show tick marks, labels, or minimal scales. Details are on the [Knob](https://cutoff.dev/audio-ui/docs/latest/components/vector/knob) and [Slider](https://cutoff.dev/audio-ui/docs/latest/components/vector/slider) pages.

## Completeness and roadmap

The vector components library is meant to expand over time: more components (e.g. XY pad, Pitch/Mod wheels and levers, additional keyboard-style controls) and more variants and easy customization (e.g. glowness, reflectiveness) as the theme system evolves. See [Theming Features](#theming-features) above for current and planned theme options. See [Introduction – Project Status](https://cutoff.dev/audio-ui/docs/latest/getting-started/introduction#project-status) for current and planned work.

## Next steps

* [Knob](https://cutoff.dev/audio-ui/docs/latest/components/vector/knob) — rotary continuous control
* [Slider](https://cutoff.dev/audio-ui/docs/latest/components/vector/slider) — linear continuous control (horizontal or vertical)
* [Button](https://cutoff.dev/audio-ui/docs/latest/components/vector/button) — toggle (latch) or momentary
* [CycleButton](https://cutoff.dev/audio-ui/docs/latest/components/vector/cycle-button) — discrete option cycling
* [Keys](https://cutoff.dev/audio-ui/docs/latest/components/vector/keys) — piano keyboard


---

# Knob

# Knob

> A rotary control component for continuous value adjustment with multiple visual variants and full theming support.

The Knob component provides a circular control for continuous value adjustment. It shares the same layout, parameter model, interaction, and accessibility as other vector components; see the [Vector introduction](https://cutoff.dev/audio-ui/docs/latest/components/vector/introduction).

* Multiple visual variants: abstract, simplest, plainCap, iconCap
* Full theming support: color, roundness, thickness customization

## Props

| Name                   | Type                                                                 | Default   | Required | Description                                                                                                                                    |
| :--------------------- | :------------------------------------------------------------------- | :-------- | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------- |
| value                  | number                                                               | -         | Yes      | Current value of the control                                                                                                                   |
| onChange               | (event: AudioControlEvent\<number>) => void                          | -         | No       | Handler for value changes                                                                                                                      |
| min                    | number                                                               | -         | No       | Minimum value (ad-hoc mode)                                                                                                                    |
| max                    | number                                                               | -         | No       | Maximum value (ad-hoc mode)                                                                                                                    |
| step                   | number                                                               | 1         | No       | Step size for value adjustments                                                                                                                |
| bipolar                | boolean                                                              | false     | No       | Whether the component should operate in bipolar mode (centered at zero)                                                                        |
| label                  | string                                                               | -         | No       | Label displayed below the component                                                                                                            |
| variant                | "abstract" \| "simplest" \| "plainCap" \| "iconCap"                  | abstract  | No       | Visual variant of the knob                                                                                                                     |
| thickness              | number                                                               | -         | No       | Thickness of the knob's stroke (normalized 0.0-1.0, maps to 1-20)                                                                              |
| openness               | number                                                               | 90        | No       | Openness of the ring in degrees (0-360º; 0º closed, 90º 3/4 open, 180º half-circle)                                                            |
| rotation               | number                                                               | 0         | No       | Optional rotation angle offset in degrees                                                                                                      |
| rotaryOverlay          | boolean                                                              | false     | No       | Whether to use RotaryImage (true) or RadialImage (false) for iconCap overlay. Only applies when variant is "iconCap" and children is provided. |
| children               | React.ReactNode                                                      | -         | No       | SVG content to display as overlay in iconCap variant. Typically an icon component that will be rendered at the center of the knob.             |
| valueAsLabel           | "labelOnly" \| "valueOnly" \| "interactive"                          | labelOnly | No       | Controls how the label and value are displayed                                                                                                 |
| color                  | string                                                               | -         | No       | Component primary color - any valid CSS color value                                                                                            |
| roundness              | number                                                               | -         | No       | Roundness for component corners (normalized 0.0-1.0)                                                                                           |
| adaptiveSize           | boolean                                                              | false     | No       | Whether the component stretches to fill its container                                                                                          |
| size                   | "xsmall" \| "small" \| "normal" \| "large" \| "xlarge"               | normal    | No       | Size of the component                                                                                                                          |
| parameter              | ContinuousParameter                                                  | -         | No       | Audio Parameter definition (Model). If provided, overrides min/max/step/label/bipolar from ad-hoc props                                        |
| valueFormatter         | (value: number, parameterDef: AudioParameter) => string \| undefined | -         | No       | Custom renderer for the value display                                                                                                          |
| interactionMode        | "drag" \| "wheel" \| "both"                                          | both      | No       | Interaction mode                                                                                                                               |
| interactionSensitivity | number                                                               | -         | No       | Sensitivity of the control (normalized value change per pixel/unit)                                                                            |
| interactionDirection   | InteractionDirection                                                 | -         | No       | Direction of the interaction                                                                                                                   |

## Basic Usage

The Knob component requires a `value` and `onChange` handler. In ad-hoc mode, you provide `min` and `max` to define the value range:

```tsx title="BasicKnob.tsx"
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";

export default function BasicKnob() {
  const [value, setValue] = useState(50);

  return (
    <Knob
      value={value}
      onChange={(e) => setValue(e.value)}
      min={0}
      max={100}
      size="large"
      label="Volume"
    />
  );
}

```

## Variants

The Knob component supports four visual variants, each with a distinct appearance:

### Simplest

A simplified version with minimal visual elements:

```tsx title="SimplestVariant.tsx"
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";

export default function SimplestVariant() {
  const [value, setValue] = useState(0.5);

  return (
    <Knob
      value={value}
      onChange={(e) => setValue(e.value)}
      variant="simplest"
      label="Resonance"
      size="large"
    />
  );
}

```

### Abstract

The default variant with a clean, minimal design. Displays the current value as text in the center:

```tsx title="AbstractVariant.tsx"
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";

export default function AbstractVariant() {
  const [value, setValue] = useState(0.5);

  return (
    <Knob
      value={value}
      onChange={(e) => setValue(e.value)}
      variant="abstract"
      label="Cutoff"
      min={0.2}
      max={1}
      size="large"
    />
  );
}

```

### PlainCap

A variant with a plain cap design:

```tsx title="PlainCapVariant.tsx"
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";

export default function PlainCapVariant() {
  const [value, setValue] = useState(0.5);

  return (
    <Knob
      value={value}
      onChange={(e) => setValue(e.value)}
      variant="plainCap"
      label="Gain"
      size="large"
    />
  );
}

```

### IconCap

The iconCap variant allows you to display custom SVG content (typically icons) at the center of the knob. The icon can rotate with the knob value or remain static:

```tsx title="IconCapVariant.tsx"
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";

export default function IconCapExample() {
  const [staticValue, setStaticValue] = useState(0);
  const [rotaryValue, setRotaryValue] = useState(0);

  return (
    <div className="flex gap-8">
      <Knob
        variant="iconCap"
        value={staticValue}
        onChange={(e) => setStaticValue(e.value)}
        min={-64}
        max={64}
        bipolar={true}
        rotaryOverlay={false}
        size="large"
        label="Static icon"
      >
        <SquareWaveIcon />
      </Knob>
      <Knob
        variant="iconCap"
        value={rotaryValue}
        onChange={(e) => setRotaryValue(e.value)}
        min={-64}
        max={64}
        bipolar={true}
        rotaryOverlay={true}
        size="large"
        label="Rotary Icon"
      >
        <SquareWaveIcon />
      </Knob>
    </div>
  );
}

```

The `rotaryOverlay` prop controls whether the icon rotates with the knob value (`true`) or remains static (`false`). The icon inherits color via `currentColor`, so it adapts to light/dark mode automatically. The example above matches the LFO knob from the AudioUI playground (iconCap with SquareWaveIcon); the playground provides sine, triangle, square, and saw wave icons you can reuse.

## Theming

Vector components share common theming features; see [Theming Features](https://cutoff.dev/audio-ui/docs/latest/components/vector/introduction#theming-features) in the Vector introduction for an overview. The Knob component supports theming through the `color` and `roundness` props, or via the global theme system:

### Color Customization

You can customize the knob's primary color using the `color` prop:

```tsx title="CustomColor.tsx"
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";

export default function CustomColor() {
  const [blueValue, setBlueValue] = useState(0.5);
  const [redValue, setRedValue] = useState(0.5);

  return (
    <div className="flex gap-8">
      <Knob
        value={blueValue}
        onChange={(e) => setBlueValue(e.value)}
        color="#3b82f6"
        size="large"
        label="Blue Knob"
      />
      <Knob
        value={redValue}
        onChange={(e) => setRedValue(e.value)}
        color="#ef4444"
        size="large"
        label="Red Knob"
      />
    </div>
  );
}

```

### Roundness

Control the roundness of the knob's corners:

```tsx title="RoundnessExample.tsx"
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";

export default function RoundnessExample() {
  const [square, setSquare] = useState(0.5);
  const [rounded, setRounded] = useState(0.5);
  const [circle, setCircle] = useState(0.5);

  return (
    <div className="flex gap-8">
      <Knob
        value={square}
        onChange={(e) => setSquare(e.value)}
        roundness={0.0}
        size="large"
        label="Square"
      />
      <Knob
        value={rounded}
        onChange={(e) => setRounded(e.value)}
        roundness={0.5}
        size="large"
        label="Rounded"
      />
      <Knob
        value={circle}
        onChange={(e) => setCircle(e.value)}
        roundness={1.0}
        size="large"
        label="Fully Rounded"
      />
    </div>
  );
}

```

> **Note:** **Value strip roundness**: For the knob's value strips (the arc that shows the current value), roundness behaves in an on/off way: `roundness={0.0}` uses a square linecap, and any other value uses a round linecap. The correct behaviour has been achieved internally but does not pass our performance requirements, so the current binary behaviour is used.

### Thickness

Adjust the stroke thickness of the knob ring:

```tsx title="ThicknessExample.tsx"
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";

export default function ThicknessExample() {
  const [thin, setThin] = useState(0.5);
  const [medium, setMedium] = useState(0.5);
  const [thick, setThick] = useState(0.5);

  return (
    <div className="flex gap-8">
      <Knob
        value={thin}
        onChange={(e) => setThin(e.value)}
        thickness={0.2}
        size="large"
        label="Thin"
      />
      <Knob
        value={medium}
        onChange={(e) => setMedium(e.value)}
        thickness={0.5}
        size="large"
        label="Medium"
      />
      <Knob
        value={thick}
        onChange={(e) => setThick(e.value)}
        thickness={0.8}
        size="large"
        label="Thick"
      />
    </div>
  );
}

```

## Ring Configuration

### Openness

The `openness` prop controls how much of the ring is visible. It accepts values in degrees (0-360º):

* `0º`: Closed ring (full circle)
* `90º`: 3/4 open (default)
* `180º`: Half-circle
* `270º`: 1/4 open

```tsx title="OpennessExample.tsx"
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";

export default function OpennessExample() {
  const [closed, setClosed] = useState(0.5);
  const [defaultOpen, setDefaultOpen] = useState(0.5);
  const [half, setHalf] = useState(0.5);

  return (
    <div className="flex gap-8">
      <Knob
        value={closed}
        onChange={(e) => setClosed(e.value)}
        openness={0}
        size="large"
        label="Closed"
      />
      <Knob
        value={defaultOpen}
        onChange={(e) => setDefaultOpen(e.value)}
        openness={90}
        size="large"
        label="Default (90º)"
      />
      <Knob
        value={half}
        onChange={(e) => setHalf(e.value)}
        openness={180}
        size="large"
        label="Half Circle"
      />
    </div>
  );
}

```

### Rotation

The `rotation` prop allows you to offset the starting angle of the ring:

```tsx title="RotationExample.tsx"
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";

export default function RotationExample() {
  const [v1, setV1] = useState(0.5);
  const [v2, setV2] = useState(0.5);
  const [v3, setV3] = useState(0.5);

  return (
    <div className="flex gap-8">
      <Knob
        value={v1}
        onChange={(e) => setV1(e.value)}
        rotation={0}
        size="large"
        label="No Rotation"
      />
      <Knob
        value={v2}
        onChange={(e) => setV2(e.value)}
        rotation={45}
        size="large"
        label="45º Rotation"
      />
      <Knob
        value={v3}
        onChange={(e) => setV3(e.value)}
        rotation={90}
        size="large"
        label="90º Rotation"
      />
    </div>
  );
}

```

## Value Display

The `valueAsLabel` prop controls how the label and value are displayed:

* `"labelOnly"`: Always shows the label (default)
* `"valueOnly"`: Always shows the value
* `"interactive"`: Shows label normally, but temporarily swaps to value during interaction

```tsx title="ValueDisplayExample.tsx"
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";

export default function ValueDisplayExample() {
  const [v1, setV1] = useState(0.5);
  const [v2, setV2] = useState(0.5);
  const [v3, setV3] = useState(0.5);

  return (
    <div className="flex gap-8">
      <Knob
        value={v1}
        onChange={(e) => setV1(e.value)}
        valueAsLabel="labelOnly"
        size="large"
        label="Label"
      />
      <Knob
        value={v2}
        onChange={(e) => setV2(e.value)}
        valueAsLabel="valueOnly"
        size="large"
        label="Volume"
      />
      <Knob
        value={v3}
        onChange={(e) => setV3(e.value)}
        valueAsLabel="interactive"
        size="large"
        label="Interactive"
      />
    </div>
  );
}

```

## Bipolar Mode

In bipolar mode, the knob visualizes values relative to a center point rather than from minimum to maximum. This is useful for controls like pan or balance:

```tsx title="BipolarExample.tsx"
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";

export default function BipolarExample() {
  const [value, setValue] = useState(0);

  return (
    <Knob
      value={value}
      onChange={(e) => setValue(e.value)}
      min={-100}
      max={100}
      bipolar={true}
      size="large"
      label="Pan"
    />
  );
}

```

## Value Formatting

You can customize how values are displayed using the `valueFormatter` prop. AudioUI provides built-in formatters for common audio parameters:

```tsx title="FormatterExample.tsx"
import { useState } from "react";
import {
  Knob,
  frequencyFormatter,
  percentageFormatter,
} from "@cutoff/audio-ui-react";

export default function FormatterExample() {
  const [frequency, setFrequency] = useState(1000);
  const [mix, setMix] = useState(50);

  return (
    <div className="flex gap-4 items-center flex-wrap">
      <Knob
        value={frequency}
        onChange={(e) => setFrequency(e.value)}
        min={20}
        max={20000}
        label="Frequency"
        size="large"
        valueFormatter={(value) => frequencyFormatter(value)}
      />
      <Knob
        value={mix}
        onChange={(e) => setMix(e.value)}
        min={0}
        max={100}
        label="Mix"
        size="large"
        valueFormatter={(value, paramDef) => {
          return percentageFormatter(value, paramDef.min, paramDef.max);
        }}
      />
    </div>
  );
}
```

## Parameter Model

Instead of using ad-hoc props (`min`, `max`, `label`), you can provide a full parameter model via the `parameter` prop. This is useful when integrating with audio parameter systems:

```tsx title="ParameterModelExample.tsx" showLineNumbers
import { Knob, createContinuousParameter } from "@cutoff/audio-ui-react";

const cutoffParam = createContinuousParameter({
  paramId: "cutoff",
  label: "Cutoff",
  min: 20,
  max: 20000,
  unit: "Hz",
  scale: "log",
  defaultValue: 1000,
});

export default function ParameterModelExample() {
  const [value, setValue] = useState(1000);

  return (
    <Knob
      parameter={cutoffParam}
      value={value}
      onChange={(e) => setValue(e.value)}
    />
  );
}
```

## Adaptive Sizing

The Knob component supports both fixed sizes and adaptive sizing. By default, the size of the component is driven by the [Size System](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/size-system) and their `size` attribute:

```tsx title="SizingExample.tsx"
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";

export default function SizingExample() {
  const [v1, setV1] = useState(0.5);
  const [v2, setV2] = useState(0.5);
  const [v3, setV3] = useState(0.5);
  const [v4, setV4] = useState(0.5);

  return (
    <div className="flex gap-4 items-center flex-wrap">
      <Knob
        value={v1}
        onChange={(e) => setV1(e.value)}
        size="small"
        label="Small"
      />
      <Knob
        value={v2}
        onChange={(e) => setV2(e.value)}
        size="normal"
        label="Normal"
      />
      <Knob
        value={v3}
        onChange={(e) => setV3(e.value)}
        size="large"
        label="Large"
      />
      <Knob
        value={v4}
        onChange={(e) => setV4(e.value)}
        size="xlarge"
        label="XLarge"
      />
    </div>
  );
}

```

Adaptive sizing allows the component to adapt to the size of its parent container, adjusting its dimensions intelligently without causing distortion (`scaleToFit` display mode). The `fill` display mode makes the component fill the entire container, potentially distorting it.

```tsx title="AdaptiveSize.tsx"
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";

export default function AdaptiveSize() {
  const [wideValue, setWideValue] = useState(0.5);
  const [tallValue, setTallValue] = useState(0.5);
  const [distortedValue, setDistortedValue] = useState(0.5);

  return (
    <div className="flex gap-6 items-center flex-wrap">
      <div className="w-48 h-16 border rounded-lg p-2">
        <Knob
          value={wideValue}
          onChange={(e) => setWideValue(e.value)}
          adaptiveSize={true}
          label="Wide"
        />
      </div>
      <div className="w-24 h-48 border rounded-lg p-2">
        <Knob
          value={tallValue}
          onChange={(e) => setTallValue(e.value)}
          adaptiveSize={true}
          label="Tall"
        />
      </div>
      <div className="w-32 h-20 border rounded-lg p-2">
        <Knob
          value={distortedValue}
          onChange={(e) => setDistortedValue(e.value)}
          adaptiveSize={true}
          displayMode="fill"
          label="Distorted"
        />
      </div>
    </div>
  );
}

```

## Interaction Modes

Control how users interact with the knob:

```tsx title="InteractionExample.tsx"
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";

export default function InteractionExample() {
  const [dragVal, setDragVal] = useState(0.5);
  const [wheelVal, setWheelVal] = useState(0.5);
  const [bothVal, setBothVal] = useState(0.5);

  return (
    <div className="flex gap-4 items-center flex-wrap">
      <Knob
        value={dragVal}
        onChange={(e) => setDragVal(e.value)}
        interactionMode="drag"
        label="Drag Only"
        size="large"
      />
      <Knob
        value={wheelVal}
        onChange={(e) => setWheelVal(e.value)}
        interactionMode="wheel"
        label="Wheel Only"
        size="large"
      />
      <Knob
        value={bothVal}
        onChange={(e) => setBothVal(e.value)}
        interactionMode="both"
        label="Drag & Wheel"
        size="large"
      />
    </div>
  );
}
```

You can also adjust the sensitivity of interactions:

```tsx title="SensitivityExample.tsx"
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";

export default function SensitivityExample() {
  const [lowVal, setLowVal] = useState(0.5);
  const [highVal, setHighVal] = useState(0.5);

  return (
    <div className="flex gap-4 items-center flex-wrap">
      <Knob
        value={lowVal}
        onChange={(e) => setLowVal(e.value)}
        interactionSensitivity={0.01}
        label="Low Sensitivity"
        size="large"
      />
      <Knob
        value={highVal}
        onChange={(e) => setHighVal(e.value)}
        interactionSensitivity={0.1}
        label="High Sensitivity"
        size="large"
      />
    </div>
  );
}
```

## Common Use Cases

### Audio Filter Control

```tsx title="FilterControl.tsx"
import { useState } from "react";
import { Knob, frequencyFormatter } from "@cutoff/audio-ui-react";

export default function FilterControl() {
  const [cutoff, setCutoff] = useState(1000);

  return (
    <Knob
      value={cutoff}
      min={20}
      max={20000}
      label="Cutoff"
      variant="abstract"
      size="large"
      valueFormatter={(value) => frequencyFormatter(value)}
      unit="Hz"
      scale="log"
      onChange={(e) => setCutoff(e.value)}
    />
  );
}
```

### Volume Control

```tsx title="VolumeControl.tsx"
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";

export default function VolumeControl() {
  const [volume, setVolume] = useState(50);

  return (
    <Knob
      value={volume}
      min={0}
      max={100}
      label="Volume"
      variant="plainCap"
      size="large"
      valueAsLabel="interactive"
      unit="%"
      onChange={(e) => setVolume(e.value)}
    />
  );
}
```

### Pan Control

```tsx title="PanControl.tsx"
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";

export default function PanControl() {
  const [pan, setPan] = useState(0);

  return (
    <Knob
      value={pan}
      min={-100}
      max={100}
      label="Pan"
      variant="simplest"
      size="large"
      bipolar={true}
      onChange={(e) => setPan(e.value)}
    />
  );
}
```

## Next Steps

* Explore the [Slider component](https://cutoff.dev/audio-ui/docs/latest/components/vector/slider) for linear controls
* Learn about [Button](https://cutoff.dev/audio-ui/docs/latest/components/vector/button) and [CycleButton](https://cutoff.dev/audio-ui/docs/latest/components/vector/cycle-button) for discrete interactions
* Check out [ImageKnob](https://cutoff.dev/audio-ui/docs/latest/components/raster/image-knob) for custom bitmap-based knobs
* Visit the [playground](https://playground.cutoff.dev) for interactive examples


---

# Slider

# Slider

> A linear slider component for continuous value adjustment with support for both horizontal and vertical orientations.

The Slider component provides a linear control for continuous value adjustment. It shares the same layout, parameter model, interaction, and accessibility as other vector components; see the [Vector introduction](https://cutoff.dev/audio-ui/docs/latest/components/vector/introduction).

* Multiple visual variants: abstract, trackless, trackfull, stripless
* Flexible orientation: horizontal and vertical layouts
* Extensive cursor customization: image-based cursors, custom styling, aspect ratio control
* Full theming support: color, roundness, thickness customization

## Props

| Name                   | Type                                                                 | Default   | Required | Description                                                                                             |
| :--------------------- | :------------------------------------------------------------------- | :-------- | :------- | :------------------------------------------------------------------------------------------------------ |
| value                  | number                                                               | -         | Yes      | Current value of the control                                                                            |
| onChange               | (event: AudioControlEvent\<number>) => void                          | -         | No       | Handler for value changes                                                                               |
| min                    | number                                                               | -         | No       | Minimum value (ad-hoc mode)                                                                             |
| max                    | number                                                               | -         | No       | Maximum value (ad-hoc mode)                                                                             |
| step                   | number                                                               | 1         | No       | Step size for value adjustments                                                                         |
| bipolar                | boolean                                                              | false     | No       | Whether the component should operate in bipolar mode (centered at zero)                                 |
| label                  | string                                                               | -         | No       | Label displayed below the component                                                                     |
| variant                | "abstract" \| "trackless" \| "trackfull" \| "stripless"              | abstract  | No       | Visual variant of the slider                                                                            |
| orientation            | "horizontal" \| "vertical"                                           | vertical  | No       | Orientation of the slider                                                                               |
| thickness              | number                                                               | 0.5       | No       | Thickness of the slider (normalized 0.0-1.0, maps to 1-50)                                              |
| valueAsLabel           | "labelOnly" \| "valueOnly" \| "interactive"                          | labelOnly | No       | Controls how the label and value are displayed                                                          |
| cursorSize             | "None" \| "Strip" \| "Track" \| "Tick" \| "Label"                    | -         | No       | Cursor size option - determines which component's width is used for the cursor                          |
| cursorAspectRatio      | number                                                               | -         | No       | Aspect ratio of the cursor                                                                              |
| cursorRoundness        | number \| string                                                     | -         | No       | Overrides the roundness factor of the cursor. Defaults to \`roundness\`                                 |
| cursorImageHref        | string                                                               | -         | No       | Optional image URL to display as cursor                                                                 |
| cursorClassName        | string                                                               | -         | No       | Optional CSS class name for the cursor                                                                  |
| cursorStyle            | React.CSSProperties                                                  | -         | No       | Optional inline styles for the cursor                                                                   |
| color                  | string                                                               | -         | No       | Component primary color - any valid CSS color value                                                     |
| roundness              | number                                                               | -         | No       | Roundness for component corners (normalized 0.0-1.0)                                                    |
| adaptiveSize           | boolean                                                              | false     | No       | Whether the component stretches to fill its container                                                   |
| size                   | "xsmall" \| "small" \| "normal" \| "large" \| "xlarge"               | normal    | No       | Size of the component                                                                                   |
| parameter              | ContinuousParameter                                                  | -         | No       | Audio Parameter definition (Model). If provided, overrides min/max/step/label/bipolar from ad-hoc props |
| valueFormatter         | (value: number, parameterDef: AudioParameter) => string \| undefined | -         | No       | Custom renderer for the value display                                                                   |
| interactionMode        | "drag" \| "wheel" \| "both"                                          | both      | No       | Interaction mode                                                                                        |
| interactionSensitivity | number                                                               | -         | No       | Sensitivity of the control (normalized value change per pixel/unit)                                     |
| interactionDirection   | InteractionDirection                                                 | -         | No       | Direction of the interaction                                                                            |

## Basic Usage

The Slider component requires a `value` and `onChange` handler. By default, sliders are vertical:

```tsx title="BasicSlider.tsx"
import { useState } from "react";
import { Slider } from "@cutoff/audio-ui-react";

export default function BasicSlider() {
  const [value, setValue] = useState(50);

  return (
    <div className="h-48">
      <Slider
        value={value}
        onChange={(e) => setValue(e.value)}
        min={0}
        max={100}
        size="large"
        label="Volume"
      />
    </div>
  );
}

```

## Orientation

Sliders can be oriented horizontally or vertically:

### Vertical Slider

The default orientation, suitable for fader-style controls:

```tsx title="VerticalSlider.tsx"
import { useState } from "react";
import { Slider } from "@cutoff/audio-ui-react";

export default function VerticalSlider() {
  const [value, setValue] = useState(0.7);

  return (
    <div className="h-48">
      <Slider
        value={value}
        onChange={(e) => setValue(e.value)}
        orientation="vertical"
        label="Volume"
        min={0}
        max={1}
        size="large"
      />
    </div>
  );
}

```

### Horizontal Slider

Useful for pan controls and other horizontal adjustments:

```tsx title="HorizontalSlider.tsx"
import { useState } from "react";
import { Slider } from "@cutoff/audio-ui-react";

export default function HorizontalSlider() {
  const [value, setValue] = useState(0);

  return (
    <div className="w-full">
      <Slider
        value={value}
        onChange={(e) => setValue(e.value)}
        orientation="horizontal"
        label="Pan"
        min={-100}
        max={100}
        bipolar={true}
        size="large"
      />
    </div>
  );
}

```

## Variants

The Slider component supports four visual variants:

### Abstract

The default variant with a clean, minimal design:

```tsx title="AbstractVariant.tsx"
import { useState } from "react";
import { Slider } from "@cutoff/audio-ui-react";

export default function AbstractVariant() {
  const [value, setValue] = useState(0.5);

  return (
    <div className="h-48">
      <Slider
        value={value}
        onChange={(e) => setValue(e.value)}
        variant="abstract"
        label="Volume"
        size="large"
      />
    </div>
  );
}

```

### Trackless

A variant without a visible track:

```tsx title="TracklessVariant.tsx"
import { useState } from "react";
import { Slider } from "@cutoff/audio-ui-react";

export default function TracklessVariant() {
  const [value, setValue] = useState(0.5);

  return (
    <div className="h-48">
      <Slider
        value={value}
        onChange={(e) => setValue(e.value)}
        variant="trackless"
        label="Gain"
        size="large"
      />
    </div>
  );
}

```

### Trackfull

A variant with a full track visible:

```tsx title="TrackfullVariant.tsx"
import { useState } from "react";
import { Slider } from "@cutoff/audio-ui-react";

export default function TrackfullVariant() {
  const [value, setValue] = useState(0.5);

  return (
    <div className="h-48">
      <Slider
        value={value}
        onChange={(e) => setValue(e.value)}
        variant="trackfull"
        label="Cutoff"
        size="large"
      />
    </div>
  );
}

```

### Stripless

A variant without a value strip:

```tsx title="StriplessVariant.tsx"
import { useState } from "react";
import { Slider } from "@cutoff/audio-ui-react";

export default function StriplessVariant() {
  const [value, setValue] = useState(0.5);

  return (
    <div className="h-48">
      <Slider
        value={value}
        onChange={(e) => setValue(e.value)}
        variant="stripless"
        label="Resonance"
        size="large"
      />
    </div>
  );
}

```

## Cursor Customization

The Slider component provides extensive options for customizing the cursor appearance:

### Cursor Size

The `cursorSize` prop determines which component's width is used for the cursor:

```tsx title="CursorSizeExample.tsx"
import { useState } from "react";
import { Slider } from "@cutoff/audio-ui-react";

export default function CursorSizeExample() {
  const [v1, setV1] = useState(0.5);
  const [v2, setV2] = useState(0.5);

  return (
    <div className="flex gap-8 h-48">
      <Slider
        value={v1}
        onChange={(e) => setV1(e.value)}
        cursorSize="Strip"
        size="large"
        label="Strip Cursor"
      />
      <Slider
        value={v2}
        onChange={(e) => setV2(e.value)}
        cursorSize="Track"
        size="large"
        label="Track Cursor"
      />
    </div>
  );
}

```

### Cursor Aspect Ratio

Control the aspect ratio of the cursor:

```tsx title="CursorAspectRatio.tsx"
import { useState } from "react";
import { Slider } from "@cutoff/audio-ui-react";

export default function CursorAspectRatio() {
  const [v1, setV1] = useState(0.5);
  const [v2, setV2] = useState(0.5);

  return (
    <div className="flex gap-8 h-48">
      <Slider
        value={v1}
        onChange={(e) => setV1(e.value)}
        cursorAspectRatio={1.0}
        size="large"
        label="Square Cursor"
      />
      <Slider
        value={v2}
        onChange={(e) => setV2(e.value)}
        cursorAspectRatio={2.0}
        size="large"
        label="Tall Cursor"
      />
    </div>
  );
}

```

### Cursor Roundness

Override the roundness of the cursor independently from the slider:

```tsx title="CursorRoundness.tsx"
import { useState } from "react";
import { Slider } from "@cutoff/audio-ui-react";

export default function CursorRoundness() {
  const [v1, setV1] = useState(0.5);
  const [v2, setV2] = useState(0.5);

  return (
    <div className="flex gap-8 h-48">
      <Slider
        value={v1}
        onChange={(e) => setV1(e.value)}
        cursorRoundness={0.0}
        size="large"
        label="Square Cursor"
      />
      <Slider
        value={v2}
        onChange={(e) => setV2(e.value)}
        cursorRoundness={1.0}
        size="large"
        label="Fully Rounded"
      />
    </div>
  );
}

```

### Custom Cursor Image

Use a custom image for the cursor:

```tsx title="CursorImage.tsx"
import { useState } from "react";
import { Slider } from "@cutoff/audio-ui-react";

export default function CursorImage() {
  const [value, setValue] = useState(0.5);

  return (
    <div className="h-48">
      <Slider
        value={value}
        onChange={(e) => setValue(e.value)}
        cursorImageHref="/images/documentation/svg-primitives-radial-light.png"
        size="large"
        label="Custom Cursor"
      />
    </div>
  );
}

```

### Cursor Styling

Apply custom CSS classes or inline styles to the cursor:

```tsx title="CursorStyling.tsx"
import { useState } from "react";
import { Slider } from "@cutoff/audio-ui-react";

export default function CursorStyling() {
  const [value, setValue] = useState(0.5);

  return (
    <div className="h-48">
      <Slider
        value={value}
        onChange={(e) => setValue(e.value)}
        cursorClassName="custom-cursor"
        cursorStyle={{ border: "2px solid #3b82f6" }}
        size="large"
        label="Styled Cursor"
      />
    </div>
  );
}

```

## Theming

Vector components share common theming features; see [Theming Features](https://cutoff.dev/audio-ui/docs/latest/components/vector/introduction#theming-features) in the Vector introduction for an overview. The Slider component supports theming through the `color` and `roundness` props:

### Color Customization

```tsx title="SliderColor.tsx"
import { useState } from "react";
import { Slider } from "@cutoff/audio-ui-react";

export default function SliderColor() {
  const [v1, setV1] = useState(0.5);
  const [v2, setV2] = useState(0.5);

  return (
    <div className="flex gap-8 h-48">
      <Slider
        value={v1}
        onChange={(e) => setV1(e.value)}
        color="#3b82f6"
        size="large"
        label="Blue Slider"
      />
      <Slider
        value={v2}
        onChange={(e) => setV2(e.value)}
        color="#ef4444"
        size="large"
        label="Red Slider"
      />
    </div>
  );
}

```

### Roundness

Control the roundness of the slider track and cursor:

```tsx title="SliderRoundness.tsx"
import { useState } from "react";
import { Slider } from "@cutoff/audio-ui-react";

export default function SliderRoundness() {
  const [v1, setV1] = useState(0.5);
  const [v2, setV2] = useState(0.5);
  const [v3, setV3] = useState(0.5);

  return (
    <div className="flex gap-8 h-48">
      <Slider
        value={v1}
        onChange={(e) => setV1(e.value)}
        roundness={0.0}
        size="large"
        label="Square"
      />
      <Slider
        value={v2}
        onChange={(e) => setV2(e.value)}
        roundness={0.5}
        size="large"
        label="Rounded"
      />
      <Slider
        value={v3}
        onChange={(e) => setV3(e.value)}
        roundness={1.0}
        size="large"
        label="Fully Rounded"
      />
    </div>
  );
}

```

### Thickness

Adjust the thickness of the slider track:

```tsx title="SliderThickness.tsx"
import { useState } from "react";
import { Slider } from "@cutoff/audio-ui-react";

export default function SliderThickness() {
  const [v1, setV1] = useState(0.5);
  const [v2, setV2] = useState(0.5);
  const [v3, setV3] = useState(0.5);

  return (
    <div className="flex gap-8 h-48">
      <Slider
        value={v1}
        onChange={(e) => setV1(e.value)}
        thickness={0.2}
        size="large"
        label="Thin"
      />
      <Slider
        value={v2}
        onChange={(e) => setV2(e.value)}
        thickness={0.5}
        size="large"
        label="Medium"
      />
      <Slider
        value={v3}
        onChange={(e) => setV3(e.value)}
        thickness={0.8}
        size="large"
        label="Thick"
      />
    </div>
  );
}

```

## Bipolar Mode

In bipolar mode, the slider visualizes values relative to a center point. This is ideal for pan controls:

```tsx title="BipolarSlider.tsx"
import { useState } from "react";
import { Slider } from "@cutoff/audio-ui-react";

export default function BipolarSlider() {
  const [value, setValue] = useState(0);

  return (
    <div className="w-full">
      <Slider
        value={value}
        onChange={(e) => setValue(e.value)}
        min={-100}
        max={100}
        bipolar={true}
        orientation="horizontal"
        size="large"
        label="Pan"
      />
    </div>
  );
}

```

## Value Display

The `valueAsLabel` prop controls how the label and value are displayed:

```tsx title="SliderValueDisplay.tsx"
import { useState } from "react";
import { Slider } from "@cutoff/audio-ui-react";

export default function SliderValueDisplay() {
  const [v1, setV1] = useState(0.5);
  const [v2, setV2] = useState(0.5);
  const [v3, setV3] = useState(0.5);

  return (
    <div className="flex gap-8 h-48">
      <Slider
        value={v1}
        onChange={(e) => setV1(e.value)}
        valueAsLabel="labelOnly"
        size="large"
        label="Volume"
      />
      <Slider
        value={v2}
        onChange={(e) => setV2(e.value)}
        valueAsLabel="valueOnly"
        size="large"
        label="Volume"
      />
      <Slider
        value={v3}
        onChange={(e) => setV3(e.value)}
        valueAsLabel="interactive"
        size="large"
        label="Volume"
      />
    </div>
  );
}

```

## Value Formatting

Customize how values are displayed using the `valueFormatter` prop:

```tsx title="SliderFormatter.tsx"
import { useState } from "react";
import { Slider, frequencyFormatter } from "@cutoff/audio-ui-react";

export default function SliderFormatterExample() {
  const [frequency, setFrequency] = useState(1000);
  const [gain, setGain] = useState(0);

  return (
    <div className="flex gap-8 items-center flex-wrap h-48">
      <Slider
        value={frequency}
        onChange={(e) => setFrequency(e.value)}
        min={20}
        max={20000}
        label="Frequency"
        valueFormatter={(value) => frequencyFormatter(value)}
        size="large"
      />
      <Slider
        value={gain}
        onChange={(e) => setGain(e.value)}
        min={-60}
        max={12}
        label="Gain"
        valueFormatter={(value) => `${Math.round(value)} dB`}
        size="large"
      />
    </div>
  );
}

```

## Parameter Model

Use a parameter model for integration with audio parameter systems. The following example uses ad-hoc props; with the full library you would use `createContinuousParameter` and the `parameter` prop:

```tsx title="SliderParameterModel.tsx"
import { useState } from "react";
import { Slider } from "@cutoff/audio-ui-react";

export default function SliderParameterModelExample() {
  const [value, setValue] = useState(50);

  return (
    <div className="h-48">
      <Slider
        min={0}
        max={100}
        label="Volume"
        value={value}
        onChange={(e) => setValue(e.value)}
        size="large"
      />
    </div>
  );
}

```

## Adaptive Sizing

The Slider component supports both fixed sizes and adaptive sizing. By default, the size of the component is driven by the [Size System](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/size-system) and their `size` attribute:

```tsx title="SliderSizing.tsx"
import { useState } from "react";
import { Slider } from "@cutoff/audio-ui-react";

export default function SliderSizing() {
  const [v1, setV1] = useState(0.5);
  const [v2, setV2] = useState(0.5);
  const [v3, setV3] = useState(0.5);
  const [v4, setV4] = useState(0.5);

  return (
    <div className="flex gap-4 items-center flex-wrap h-64">
      <Slider
        value={v1}
        onChange={(e) => setV1(e.value)}
        size="small"
        label="Small"
      />
      <Slider
        value={v2}
        onChange={(e) => setV2(e.value)}
        size="normal"
        label="Normal"
      />
      <Slider
        value={v3}
        onChange={(e) => setV3(e.value)}
        size="large"
        label="Large"
      />
      <Slider
        value={v4}
        onChange={(e) => setV4(e.value)}
        size="xlarge"
        label="XLarge"
      />
    </div>
  );
}

```

Adaptive sizing allows the component to adapt to the size of its parent container, adjusting its dimensions intelligently without causing distortion (`scaleToFit` display mode). The `fill` display mode makes the component fill the entire container, potentially distorting it.

```tsx title="SliderAdaptiveSize.tsx"
import { useState } from "react";
import { Slider } from "@cutoff/audio-ui-react";

export default function SliderAdaptiveSize() {
  const [wideValue, setWideValue] = useState(0.5);
  const [tallValue, setTallValue] = useState(0.5);
  const [distortedValue, setDistortedValue] = useState(0.5);

  return (
    <div className="flex gap-6 items-center flex-wrap h-48">
      <div className="w-48 h-16 border rounded-lg p-2">
        <Slider
          value={wideValue}
          onChange={(e) => setWideValue(e.value)}
          adaptiveSize={true}
          orientation="horizontal"
          label="Wide"
        />
      </div>
      <div className="w-24 h-48 border rounded-lg p-2">
        <Slider
          value={tallValue}
          onChange={(e) => setTallValue(e.value)}
          adaptiveSize={true}
          label="Tall"
        />
      </div>
      <div className="w-32 h-20 border rounded-lg p-2">
        <Slider
          value={distortedValue}
          onChange={(e) => setDistortedValue(e.value)}
          adaptiveSize={true}
          displayMode="fill"
          label="Distorted"
        />
      </div>
    </div>
  );
}

```

## Interaction Modes

Control how users interact with the slider:

```tsx title="SliderInteraction.tsx"
import { useState } from "react";
import { Slider } from "@cutoff/audio-ui-react";

export default function SliderInteractionExample() {
  const [dragVal, setDragVal] = useState(0.5);
  const [wheelVal, setWheelVal] = useState(0.5);
  const [bothVal, setBothVal] = useState(0.5);

  return (
    <div className="flex gap-4 items-center flex-wrap h-48">
      <Slider
        value={dragVal}
        onChange={(e) => setDragVal(e.value)}
        interactionMode="drag"
        label="Drag Only"
        size="large"
      />
      <Slider
        value={wheelVal}
        onChange={(e) => setWheelVal(e.value)}
        interactionMode="wheel"
        label="Wheel Only"
        size="large"
      />
      <Slider
        value={bothVal}
        onChange={(e) => setBothVal(e.value)}
        interactionMode="both"
        label="Drag & Wheel"
        size="large"
      />
    </div>
  );
}
```

Adjust the sensitivity of interactions:

```tsx title="SliderSensitivity.tsx"
import { useState } from "react";
import { Slider } from "@cutoff/audio-ui-react";

export default function SliderSensitivityExample() {
  const [lowVal, setLowVal] = useState(0.5);
  const [highVal, setHighVal] = useState(0.5);

  return (
    <div className="flex gap-4 items-center flex-wrap h-48">
      <Slider
        value={lowVal}
        onChange={(e) => setLowVal(e.value)}
        interactionSensitivity={0.01}
        label="Low Sensitivity"
        size="large"
      />
      <Slider
        value={highVal}
        onChange={(e) => setHighVal(e.value)}
        interactionSensitivity={0.1}
        label="High Sensitivity"
        size="large"
      />
    </div>
  );
}
```

## Common Patterns

### Volume Fader

A typical vertical volume fader:

```tsx title="VolumeFader.tsx"
import { useState } from "react";
import { Slider } from "@cutoff/audio-ui-react";

export default function VolumeFader() {
  const [volume, setVolume] = useState(70);

  return (
    <div className="h-48">
      <Slider
        value={volume}
        onChange={(e) => setVolume(e.value)}
        orientation="vertical"
        variant="trackfull"
        min={0}
        max={100}
        label="Volume"
        size="large"
        valueFormatter={(value) => `${Math.round(value)}%`}
      />
    </div>
  );
}
```

### Pan Control

A horizontal pan control with bipolar mode:

```tsx title="PanControl.tsx"
import { useState } from "react";
import { Slider } from "@cutoff/audio-ui-react";

export default function PanControl() {
  const [pan, setPan] = useState(0);

  return (
    <Slider
      value={pan}
      onChange={(e) => setPan(e.value)}
      orientation="horizontal"
      min={-100}
      max={100}
      bipolar={true}
      label="Pan"
      size="large"
      valueFormatter={(value) => {
        if (value === 0) return "C";
        return value > 0 ? `R${Math.abs(value)}` : `L${Math.abs(value)}`;
      }}
    />
  );
}
```

## Next Steps

* Explore the [Knob component](https://cutoff.dev/audio-ui/docs/latest/components/vector/knob) for rotary controls
* Learn about [Button](https://cutoff.dev/audio-ui/docs/latest/components/vector/button) and [CycleButton](https://cutoff.dev/audio-ui/docs/latest/components/vector/cycle-button) for discrete interactions
* Check out [FilmStripContinuousControl](https://cutoff.dev/audio-ui/docs/latest/components/raster/filmstrip-continuous) for bitmap-based sliders
* Visit the [playground](https://playground.cutoff.dev) for interactive examples


---

# Button

# Button

> A button component for audio applications supporting both toggle (latch) and momentary modes with proper event handling.

The Button component provides a customizable button for audio applications. It shares the same layout, parameter model, interaction, and accessibility as other vector components; see the [Vector introduction](https://cutoff.dev/audio-ui/docs/latest/components/vector/introduction).

* Two operation modes: Latch (toggle) and momentary
* Full theming support: color, roundness customization
* Drag interactions: Proper handling of global mouse events to ensure reliable release behavior, and allow easy implementations of, for example, step sequencers and pads

## Props

| Name             | Type                                                   | Default    | Required | Description                                                                                                                                        |
| :--------------- | :----------------------------------------------------- | :--------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------- |
| value            | boolean                                                | -          | Yes      | Current value of the button (true = pressed/active, false = released/inactive)                                                                     |
| onChange         | (event: AudioControlEvent\<boolean>) => void           | -          | No       | Handler for value changes                                                                                                                          |
| latch            | boolean                                                | false      | No       | Whether the button should latch (toggle between states) or momentary (only active while pressed). Ad-hoc mode only, ignored if parameter provided. |
| label            | string                                                 | -          | No       | Label displayed below the component                                                                                                                |
| color            | string                                                 | -          | No       | Component primary color - any valid CSS color value                                                                                                |
| roundness        | number                                                 | -          | No       | Roundness for component corners (normalized 0.0-1.0)                                                                                               |
| adaptiveSize     | boolean                                                | false      | No       | Whether the component stretches to fill its container                                                                                              |
| size             | "xsmall" \| "small" \| "normal" \| "large" \| "xlarge" | normal     | No       | Size of the component                                                                                                                              |
| displayMode      | "scaleToFit" \| "fill"                                 | scaleToFit | No       | Layout mode for the control                                                                                                                        |
| labelMode        | "none" \| "hidden" \| "visible"                        | visible    | No       | Visibility of the label row                                                                                                                        |
| labelPosition    | "above" \| "below"                                     | below      | No       | Vertical position of the label relative to the control                                                                                             |
| labelAlign       | "start" \| "center" \| "end"                           | center     | No       | Horizontal alignment of the label text                                                                                                             |
| labelOverflow    | "ellipsis" \| "abbreviate" \| "auto"                   | auto       | No       | How to handle label text overflow                                                                                                                  |
| labelHeightUnits | number                                                 | -          | No       | Label height in the same units as SVG viewBox height                                                                                               |
| parameter        | BooleanParameter                                       | -          | No       | Audio Parameter definition (Model). If provided, overrides label/latch from ad-hoc props                                                           |
| paramId          | string                                                 | -          | No       | Identifier for the parameter this control represents                                                                                               |
| midiResolution   | MidiResolution                                         | 7          | No       | MIDI resolution in bits (ad-hoc mode, ignored if parameter provided)                                                                               |
| onClick          | React.MouseEventHandler                                | -          | No       | Click event handler                                                                                                                                |
| onMouseDown      | React.MouseEventHandler                                | -          | No       | Mouse down event handler                                                                                                                           |
| onMouseUp        | React.MouseEventHandler                                | -          | No       | Mouse up event handler                                                                                                                             |
| onMouseEnter     | React.MouseEventHandler                                | -          | No       | Mouse enter event handler                                                                                                                          |
| onMouseLeave     | React.MouseEventHandler                                | -          | No       | Mouse leave event handler                                                                                                                          |
| className        | string                                                 | -          | No       | Additional CSS classes                                                                                                                             |
| style            | React.CSSProperties                                    | -          | No       | Additional inline styles                                                                                                                           |

## Basic Usage

The Button component requires a `value` (boolean) and `onChange` handler. The button operates in two modes: latch (toggle) or momentary.

### Latch Mode (Toggle)

In latch mode, the button toggles between pressed and released states:

```tsx title="LatchButton.tsx"
import { useState } from "react";
import { Button } from "@cutoff/audio-ui-react";

export default function LatchButtonExample() {
  const [isOn, setIsOn] = useState(false);

  return (
    <Button
      label="Power"
      latch={true}
      value={isOn}
      onChange={(e) => setIsOn(e.value)}
      size="large"
    />
  );
}

```

### Momentary Mode

In momentary mode, the button is only active while being pressed:

```tsx title="MomentaryButton.tsx"
import { useState } from "react";
import { Button } from "@cutoff/audio-ui-react";

export default function MomentaryButtonExample() {
  const [isPressed, setIsPressed] = useState(false);

  return (
    <Button
      label="Record"
      latch={false}
      value={isPressed}
      onChange={(e) => setIsPressed(e.value)}
      size="large"
    />
  );
}

```

## Modes of Operation

The Button component supports two modes of operation:

### Ad-Hoc Mode (Props Only)

In ad-hoc mode, you provide individual props (`label`, `latch`, etc.) and the component creates the parameter model internally:

```tsx title="AdHocButton.tsx"
import { useState } from "react";
import { Button } from "@cutoff/audio-ui-react";

export default function AdHocButtonExample() {
  const [isOn, setIsOn] = useState(false);

  return (
    <Button
      label="Power"
      latch={true}
      value={isOn}
      onChange={(e) => setIsOn(e.value)}
      size="large"
    />
  );
}
```

### Strict Mode (Parameter Only)

In strict mode, you provide a full parameter model via the `parameter` prop. The following example uses ad-hoc props; with the full library you would use `createBooleanParameter` and the `parameter` prop:

```tsx title="StrictModeButton.tsx"
import { useState } from "react";
import { Button } from "@cutoff/audio-ui-react";

export default function StrictModeButtonExample() {
  const [isOn, setIsOn] = useState(false);

  return (
    <Button
      label="Power"
      latch={true}
      value={isOn}
      onChange={(e) => setIsOn(e.value)}
      size="large"
    />
  );
}
```

## Theming

Vector components share common theming features; see [Theming Features](https://cutoff.dev/audio-ui/docs/latest/components/vector/introduction#theming-features) in the Vector introduction for an overview. The Button component supports theming through the `color` and `roundness` props:

### Color Customization

```tsx title="ButtonColor.tsx"
import { useState } from "react";
import { Button } from "@cutoff/audio-ui-react";

export default function ButtonColor() {
  const [v1, setV1] = useState(false);
  const [v2, setV2] = useState(false);

  return (
    <div className="flex gap-8">
      <Button
        label="Power"
        latch={true}
        value={v1}
        onChange={(e) => setV1(e.value)}
        color="#3b82f6"
        size="large"
      />
      <Button
        label="Record"
        latch={false}
        value={v2}
        onChange={(e) => setV2(e.value)}
        color="#ef4444"
        size="large"
      />
    </div>
  );
}

```

### Roundness

Control the roundness of the button corners:

```tsx title="ButtonRoundness.tsx"
import { useState } from "react";
import { Button } from "@cutoff/audio-ui-react";

export default function ButtonRoundness() {
  const [v1, setV1] = useState(false);
  const [v2, setV2] = useState(false);
  const [v3, setV3] = useState(false);

  return (
    <div className="flex gap-8">
      <Button
        label="Square"
        latch={true}
        value={v1}
        onChange={(e) => setV1(e.value)}
        roundness={0.0}
        size="large"
      />
      <Button
        label="Rounded"
        latch={true}
        value={v2}
        onChange={(e) => setV2(e.value)}
        roundness={0.5}
        size="large"
      />
      <Button
        label="Fully Rounded"
        latch={true}
        value={v3}
        onChange={(e) => setV3(e.value)}
        roundness={1.0}
        size="large"
      />
    </div>
  );
}

```

## Adaptive Sizing

The Button component supports both fixed sizes and adaptive sizing. By default, the size of the component is driven by the [Size System](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/size-system) and their `size` attribute:

```tsx title="ButtonSizing.tsx"
import { useState } from "react";
import { Button } from "@cutoff/audio-ui-react";

export default function ButtonSizing() {
  const [v1, setV1] = useState(false);
  const [v2, setV2] = useState(false);
  const [v3, setV3] = useState(false);
  const [v4, setV4] = useState(false);

  return (
    <div className="flex gap-4 items-center flex-wrap">
      <Button
        label="Small"
        latch={true}
        value={v1}
        onChange={(e) => setV1(e.value)}
        size="small"
      />
      <Button
        label="Normal"
        latch={true}
        value={v2}
        onChange={(e) => setV2(e.value)}
        size="normal"
      />
      <Button
        label="Large"
        latch={true}
        value={v3}
        onChange={(e) => setV3(e.value)}
        size="large"
      />
      <Button
        label="XLarge"
        latch={true}
        value={v4}
        onChange={(e) => setV4(e.value)}
        size="xlarge"
      />
    </div>
  );
}

```

Adaptive sizing allows the component to adapt to the size of its parent container, adjusting its dimensions intelligently without causing distortion (`scaleToFit` display mode). The `fill` display mode makes the component fill the entire container, potentially distorting it.

```tsx title="ButtonAdaptiveSize.tsx"
import { useState } from "react";
import { Button } from "@cutoff/audio-ui-react";

export default function ButtonAdaptiveSize() {
  const [v1, setV1] = useState(false);
  const [v2, setV2] = useState(false);
  const [v3, setV3] = useState(false);

  return (
    <div className="flex gap-6 items-center flex-wrap">
      <div className="w-32 h-8 border rounded-lg p-2">
        <Button
          label="Wide"
          latch={true}
          value={v1}
          onChange={(e) => setV1(e.value)}
          adaptiveSize={true}
        />
      </div>
      <div className="w-12 h-32 border rounded-lg p-2">
        <Button
          label="Tall"
          latch={true}
          value={v2}
          onChange={(e) => setV2(e.value)}
          adaptiveSize={true}
        />
      </div>
      <div className="w-24 h-16 border rounded-lg p-2">
        <Button
          label="Distorted"
          latch={true}
          value={v3}
          onChange={(e) => setV3(e.value)}
          adaptiveSize={true}
          displayMode="fill"
        />
      </div>
    </div>
  );
}

```

## Label Configuration

Control how the label is displayed:

### Label Position

```tsx title="LabelPosition.tsx"
import { useState } from "react";
import { Button } from "@cutoff/audio-ui-react";

export default function LabelPositionExample() {
  const [aboveOn, setAboveOn] = useState(false);
  const [belowOn, setBelowOn] = useState(false);

  return (
    <div className="flex gap-4 items-center flex-wrap">
      <Button
        label="Above"
        latch={true}
        value={aboveOn}
        onChange={(e) => setAboveOn(e.value)}
        labelPosition="above"
        size="large"
      />
      <Button
        label="Below"
        latch={true}
        value={belowOn}
        onChange={(e) => setBelowOn(e.value)}
        labelPosition="below"
        size="large"
      />
    </div>
  );
}
```

### Label Alignment

```tsx title="LabelAlignment.tsx"
import { useState } from "react";
import { Button } from "@cutoff/audio-ui-react";

export default function LabelAlignmentExample() {
  const [startOn, setStartOn] = useState(false);
  const [centerOn, setCenterOn] = useState(false);
  const [endOn, setEndOn] = useState(false);

  return (
    <div className="flex gap-4 items-center flex-wrap">
      <Button
        label="Start"
        latch={true}
        value={startOn}
        onChange={(e) => setStartOn(e.value)}
        labelAlign="start"
        size="large"
      />
      <Button
        label="Center"
        latch={true}
        value={centerOn}
        onChange={(e) => setCenterOn(e.value)}
        labelAlign="center"
        size="large"
      />
      <Button
        label="End"
        latch={true}
        value={endOn}
        onChange={(e) => setEndOn(e.value)}
        labelAlign="end"
        size="large"
      />
    </div>
  );
}
```

### Label Visibility

```tsx title="LabelVisibility.tsx"
import { useState } from "react";
import { Button } from "@cutoff/audio-ui-react";

export default function LabelVisibilityExample() {
  const [visibleOn, setVisibleOn] = useState(false);
  const [hiddenOn, setHiddenOn] = useState(false);
  const [noneOn, setNoneOn] = useState(false);

  return (
    <div className="flex gap-4 items-center flex-wrap">
      <Button
        label="Visible"
        latch={true}
        value={visibleOn}
        onChange={(e) => setVisibleOn(e.value)}
        labelMode="visible"
        size="large"
      />
      <Button
        label="Hidden"
        latch={true}
        value={hiddenOn}
        onChange={(e) => setHiddenOn(e.value)}
        labelMode="hidden"
        size="large"
      />
      <Button
        label="None"
        latch={true}
        value={noneOn}
        onChange={(e) => setNoneOn(e.value)}
        labelMode="none"
        size="large"
      />
    </div>
  );
}
```

## Event Handling

The Button component provides standard mouse event handlers:

```tsx title="ButtonEvents.tsx"
import { useState } from "react";
import { Button } from "@cutoff/audio-ui-react";

export default function ButtonEventsExample() {
  const [isOn, setIsOn] = useState(false);

  return (
    <Button
      label="Interactive"
      latch={true}
      value={isOn}
      onChange={(e) => setIsOn(e.value)}
      size="large"
      onClick={() => console.log("Clicked")}
      onMouseDown={() => console.log("Mouse down")}
      onMouseUp={() => console.log("Mouse up")}
      onMouseEnter={() => console.log("Mouse enter")}
      onMouseLeave={() => console.log("Mouse leave")}
    />
  );
}
```

## Global Mouse Event Handling

The Button component handles global mouse events for momentary buttons to ensure reliable release behavior. This means that even if you drag the mouse outside the button before releasing, the button will still properly detect the release event.

This is particularly important for momentary buttons used in audio applications where precise timing is critical.

## Common Patterns

### Power Button

A typical power toggle button:

```tsx title="PowerButton.tsx"
import { useState } from "react";
import { Button } from "@cutoff/audio-ui-react";

export default function PowerButtonExample() {
  const [isPowered, setIsPowered] = useState(false);

  return (
    <Button
      label="Power"
      latch={true}
      value={isPowered}
      onChange={(e) => setIsPowered(e.value)}
      color={isPowered ? "#10b981" : "#6b7280"}
      size="large"
    />
  );
}
```

### Record Button

A momentary record button:

```tsx title="RecordButton.tsx"
import { useState } from "react";
import { Button } from "@cutoff/audio-ui-react";

export default function RecordButtonExample() {
  const [isRecording, setIsRecording] = useState(false);

  return (
    <Button
      label="Record"
      latch={false}
      value={isRecording}
      onChange={(e) => setIsRecording(e.value)}
      color={isRecording ? "#ef4444" : "#6b7280"}
      size="large"
    />
  );
}
```

### Button Group

Multiple buttons in a group:

```tsx title="ButtonGroup.tsx"
import { useState } from "react";
import { Button } from "@cutoff/audio-ui-react";

export default function ButtonGroupExample() {
  const [power, setPower] = useState(false);
  const [record, setRecord] = useState(false);
  const [play, setPlay] = useState(false);

  return (
    <div className="flex gap-4 items-center flex-wrap">
      <Button
        label="Power"
        latch={true}
        value={power}
        onChange={(e) => setPower(e.value)}
        size="large"
      />
      <Button
        label="Record"
        latch={false}
        value={record}
        onChange={(e) => setRecord(e.value)}
        size="large"
      />
      <Button
        label="Play"
        latch={true}
        value={play}
        onChange={(e) => setPlay(e.value)}
        size="large"
      />
    </div>
  );
}

```

## Parameter Model

Use a parameter model for integration with audio parameter systems. The following example uses ad-hoc props; with the full library you would use `createBooleanParameter` and the `parameter` prop:

```tsx title="ButtonParameterModel.tsx"
import { useState } from "react";
import { Button } from "@cutoff/audio-ui-react";

export default function ButtonParameterModelExample() {
  const [value, setValue] = useState(false);

  return (
    <Button
      label="Power"
      latch={true}
      value={value}
      onChange={(e) => setValue(e.value)}
      size="large"
    />
  );
}
```

## Common Use Cases

### Power Switch

```tsx title="PowerSwitch.tsx"
import { useState } from "react";
import { Button } from "@cutoff/audio-ui-react";

export default function PowerSwitchExample() {
  const [isOn, setIsOn] = useState(false);

  return (
    <Button
      value={isOn}
      latch={true}
      label="Power"
      onChange={(e) => setIsOn(e.value)}
      size="large"
    />
  );
}
```

### Record Button

```tsx title="RecordButton.tsx"
import { useState } from "react";
import { Button } from "@cutoff/audio-ui-react";

export default function RecordButtonUseCaseExample() {
  const [isRecording, setIsRecording] = useState(false);

  return (
    <Button
      value={isRecording}
      latch={false}
      label="Record"
      onChange={(e) => setIsRecording(e.value)}
      size="large"
    />
  );
}
```

### Effect Bypass

```tsx title="EffectBypass.tsx"
import { useState } from "react";
import { Button } from "@cutoff/audio-ui-react";

export default function EffectBypassExample() {
  const [isBypassed, setIsBypassed] = useState(false);

  return (
    <Button
      value={isBypassed}
      latch={true}
      label="Bypass"
      onChange={(e) => setIsBypassed(e.value)}
      size="large"
    />
  );
}
```

## Next Steps

* Explore the [CycleButton component](https://cutoff.dev/audio-ui/docs/latest/components/vector/cycle-button) for discrete option selection
* Learn about [Knob](https://cutoff.dev/audio-ui/docs/latest/components/vector/knob) and [Slider](https://cutoff.dev/audio-ui/docs/latest/components/vector/slider) for continuous controls
* Check out [FilmStripBooleanControl](https://cutoff.dev/audio-ui/docs/latest/components/raster/filmstrip-boolean) or [ImageSwitch](https://cutoff.dev/audio-ui/docs/latest/components/raster/image-switch) for bitmap-based buttons
* Visit the [playground](https://playground.cutoff.dev) for interactive examples


---

# CycleButton

# CycleButton

> A discrete interaction control that cycles through a set of options with support for custom visual content and multiple operation modes.

The CycleButton component provides a discrete interaction control that cycles through a set of options. It shares the same layout, parameter model, interaction, and accessibility as other vector components; see the [Vector introduction](https://cutoff.dev/audio-ui/docs/latest/components/vector/introduction).

* Multiple visual variants: abstract, simplest, plainCap, iconCap
* Four modes of operation: Ad-hoc (children), strict (parameter), hybrid, and options prop
* Full theming support: color, roundness, thickness customization

**Important**: This control supports discrete interaction only (click to cycle, keyboard to step). It does not support continuous interaction (drag/wheel).

## Props

| Name             | Type                                                                    | Default    | Required | Description                                                                                                                                                                                                               |
| :--------------- | :---------------------------------------------------------------------- | :--------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| value            | string \| number                                                        | -          | No       | Current value of the control (Controlled mode)                                                                                                                                                                            |
| defaultValue     | string \| number                                                        | -          | No       | Default value of the component (Uncontrolled mode)                                                                                                                                                                        |
| onChange         | (event: AudioControlEvent\<string \| number>) => void                   | -          | No       | Handler for value changes                                                                                                                                                                                                 |
| label            | string                                                                  | -          | No       | Label displayed below the component                                                                                                                                                                                       |
| options          | DiscreteOption\[]                                                       | -          | No       | Option definitions for the parameter model (Ad-Hoc mode). Defines the parameter structure (value, label, midiValue). Does NOT provide visual content - use children (OptionView components) for that.                     |
| children         | React.ReactNode                                                         | -          | No       | Child elements (OptionView components) for visual content mapping. Provides ReactNodes for rendering (icons, text, custom components). Does NOT define the parameter model - use options prop or parameter prop for that. |
| renderOption     | (option: { value: string \| number; label: string }) => React.ReactNode | -          | No       | Custom renderer for options (used when parameter is provided but no children map exists)                                                                                                                                  |
| thickness        | number                                                                  | 0.4        | No       | Thickness of the stroke (normalized 0.0-1.0, maps to 1-20). Used by rotary variant.                                                                                                                                       |
| openness         | number                                                                  | 90         | No       | Openness of the ring in degrees (0-360º; 0º closed, 90º 3/4 open, 180º half-circle)                                                                                                                                       |
| rotation         | number                                                                  | 0          | No       | Optional rotation angle offset in degrees                                                                                                                                                                                 |
| color            | string                                                                  | -          | No       | Component primary color - any valid CSS color value                                                                                                                                                                       |
| roundness        | number                                                                  | -          | No       | Roundness for component corners (normalized 0.0-1.0)                                                                                                                                                                      |
| adaptiveSize     | boolean                                                                 | false      | No       | Whether the component stretches to fill its container                                                                                                                                                                     |
| size             | "xsmall" \| "small" \| "normal" \| "large" \| "xlarge"                  | normal     | No       | Size of the component                                                                                                                                                                                                     |
| displayMode      | "scaleToFit" \| "fill"                                                  | scaleToFit | No       | Layout mode for the control                                                                                                                                                                                               |
| labelMode        | "none" \| "hidden" \| "visible"                                         | visible    | No       | Visibility of the label row                                                                                                                                                                                               |
| labelPosition    | "above" \| "below"                                                      | below      | No       | Vertical position of the label relative to the control                                                                                                                                                                    |
| labelAlign       | "start" \| "center" \| "end"                                            | center     | No       | Horizontal alignment of the label text                                                                                                                                                                                    |
| labelOverflow    | "ellipsis" \| "abbreviate" \| "auto"                                    | auto       | No       | How to handle label text overflow                                                                                                                                                                                         |
| labelHeightUnits | number                                                                  | -          | No       | Label height in the same units as SVG viewBox height                                                                                                                                                                      |
| parameter        | DiscreteParameter                                                       | -          | No       | Audio Parameter definition (Model). If provided, overrides label/options from ad-hoc props                                                                                                                                |
| paramId          | string                                                                  | -          | No       | Identifier for the parameter this control represents                                                                                                                                                                      |
| midiResolution   | MidiResolution                                                          | 7          | No       | MIDI resolution in bits (ad-hoc mode, ignored if parameter provided)                                                                                                                                                      |
| midiMapping      | "spread" \| "sequential" \| "custom"                                    | spread     | No       | MIDI mapping strategy (ad-hoc mode, ignored if parameter provided)                                                                                                                                                        |
| onClick          | React.MouseEventHandler                                                 | -          | No       | Click event handler                                                                                                                                                                                                       |
| onMouseDown      | React.MouseEventHandler                                                 | -          | No       | Mouse down event handler                                                                                                                                                                                                  |
| onMouseUp        | React.MouseEventHandler                                                 | -          | No       | Mouse up event handler                                                                                                                                                                                                    |
| onMouseEnter     | React.MouseEventHandler                                                 | -          | No       | Mouse enter event handler                                                                                                                                                                                                 |
| onMouseLeave     | React.MouseEventHandler                                                 | -          | No       | Mouse leave event handler                                                                                                                                                                                                 |
| className        | string                                                                  | -          | No       | Additional CSS classes                                                                                                                                                                                                    |
| style            | React.CSSProperties                                                     | -          | No       | Additional inline styles                                                                                                                                                                                                  |

## Understanding Options vs Children

The CycleButton component distinguishes between two concepts:

* **`options` prop**: Defines the parameter model (value, label, midiValue). Used for parameter structure.
* **`children` (OptionView components)**: Provides visual content (ReactNodes) for rendering. Used for display.

These serve different purposes and can be used together:

* Use `options` when you have data-driven option definitions
* Use `children` when you want to provide custom visual content (icons, styled text, etc.)
* Use both: `options` for the model, `children` for visuals (matched by value)

> **Note:** **Basic use**: For basic use of CycleButton, and especially when using ad-hoc props (no `parameter` prop), prefer **OptionView** children. In that case the option set is inferred from the OptionViews: you declare each option once as `<OptionView value="...">...</OptionView>`, and the same children are used for display. Use the `options` prop or a full parameter when you need explicit parameter structure (e.g. data-driven options, MIDI mapping). For a full disambiguation of options vs OptionView, see [Options vs OptionView](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/options-and-optionview).

## Modes of Operation

The CycleButton component supports four modes of operation:

### 1. Ad-Hoc Mode (Options prop)

Model from `options` prop, visual from `children` (if provided) or default rendering:

```tsx title="OptionsPropMode.tsx"
import { CycleButton } from "@cutoff/audio-ui-react";

export default function OptionsPropModeExample() {
  const [waveform, setWaveform] = useState("sine");

  return (
    <CycleButton
      options={[
        { value: "sine", label: "Sine Wave" },
        { value: "square", label: "Square Wave" },
        { value: "sawtooth", label: "Sawtooth Wave" },
        { value: "triangle", label: "Triangle Wave" },
      ]}
      value={waveform}
      onChange={(e) => setWaveform(e.value)}
      label="Waveform"
      size="large"
    />
  );
}

```

### 2. Ad-Hoc Mode (Children only)

Model inferred from OptionView children, visual from children:

```tsx title="ChildrenOnlyMode.tsx"
import { CycleButton, OptionView } from "@cutoff/audio-ui-react";

export default function ChildrenOnlyModeExample() {
  const [waveform, setWaveform] = useState("sine");

  return (
    <CycleButton
      defaultValue="sine"
      onChange={(e) => setWaveform(e.value)}
      label="Waveform"
      size="large"
    >
      <OptionView value="sine">Sine</OptionView>
      <OptionView value="square">Square</OptionView>
      <OptionView value="sawtooth">Saw</OptionView>
      <OptionView value="triangle">Tri</OptionView>
    </CycleButton>
  );
}

```

### 3. Strict Mode (Parameter only)

Model from `parameter` prop, visual via `renderOption` callback. The following example uses children (OptionView); with the full library you would use `createDiscreteParameter` and the `parameter` prop:

```tsx title="StrictMode.tsx"
import { useState } from "react";
import { CycleButton, OptionView } from "@cutoff/audio-ui-react";

export default function StrictModeExample() {
  const [waveform, setWaveform] = useState("sine");

  return (
    <CycleButton
      value={waveform}
      onChange={(e) => setWaveform(e.value)}
      label="Waveform"
      size="large"
    >
      <OptionView value="sine">Sine</OptionView>
      <OptionView value="square">Square</OptionView>
      <OptionView value="sawtooth">Saw</OptionView>
      <OptionView value="triangle">Tri</OptionView>
    </CycleButton>
  );
}
```

### 4. Hybrid Mode (Parameter + Children)

Model from `parameter` prop, visual from children (matched by value):

```tsx title="HybridMode.tsx"
import { CycleButton, OptionView, createDiscreteParameter } from "@cutoff/audio-ui-react";

const waveformParam = createDiscreteParameter({
  paramId: "waveform",
  label: "Waveform",
  options: [
    { value: "sine", label: "Sine Wave", midiValue: 0 },
    { value: "square", label: "Square Wave", midiValue: 1 },
    { value: "sawtooth", label: "Sawtooth Wave", midiValue: 2 },
    { value: "triangle", label: "Triangle Wave", midiValue: 3 },
  ],
  defaultValue: "sine",
});

export default function HybridModeExample() {
  const [waveform, setWaveform] = useState("sine");

  return (
    <CycleButton
      parameter={waveformParam}
      value={waveform}
      onChange={(e) => setWaveform(e.value)}
      size="large"
    >
      <OptionView value="sine">Sine</OptionView>
      <OptionView value="square">Square</OptionView>
      <OptionView value="sawtooth">Saw</OptionView>
      <OptionView value="triangle">Tri</OptionView>
    </CycleButton>
  );
}

```

## Using OptionView

The `OptionView` component is used to provide visual content for each option. It accepts any ReactNode as children:

### Text Content

```tsx title="TextOptionView.tsx"
import { CycleButton, OptionView } from "@cutoff/audio-ui-react";

export default function TextOptionViewExample() {
  return (
    <CycleButton defaultValue="sine" label="Waveform" size="large">
      <OptionView value="sine">Sine</OptionView>
      <OptionView value="square">Square</OptionView>
      <OptionView value="sawtooth">Saw</OptionView>
      <OptionView value="triangle">Tri</OptionView>
    </CycleButton>
  );
}
```

### Icon Content

```tsx title="IconOptionView.tsx"
import { CycleButton, OptionView } from "@cutoff/audio-ui-react";

export default function IconOptionViewExample() {
  return (
    <CycleButton defaultValue="sine" label="Waveform" size="large">
      <OptionView value="sine"><SineWaveIcon /></OptionView>
      <OptionView value="square"><SquareWaveIcon /></OptionView>
      <OptionView value="sawtooth"><SawWaveIcon /></OptionView>
      <OptionView value="triangle"><TriangleWaveIcon /></OptionView>
    </CycleButton>
  );
}
```

### Custom Components

```tsx title="CustomOptionView.tsx"
import { CycleButton, OptionView } from "@cutoff/audio-ui-react";

export default function CustomOptionViewExample() {
  return (
    <CycleButton defaultValue="sine" label="Waveform" size="large">
      <OptionView value="sine">
        <div className="flex items-center gap-1">
          <SineWaveIcon />
          <span>Sine</span>
        </div>
      </OptionView>
      <OptionView value="square">
        <div className="flex items-center gap-1">
          <SquareWaveIcon />
          <span>Square</span>
        </div>
      </OptionView>
    </CycleButton>
  );
}
```

```tsx title="OptionViewExample.tsx"
import { CycleButton, OptionView } from "@cutoff/audio-ui-react";

export default function OptionViewExample() {
  const [waveform, setWaveform] = useState("sine");
  return (
    <CycleButton value={waveform} onChange={(e) => setWaveform(e.value)} label="Waveform" size="large">
      <OptionView value="sine">Sine</OptionView>
      <OptionView value="square">Square</OptionView>
      <OptionView value="sawtooth">Saw</OptionView>
      <OptionView value="triangle">Tri</OptionView>
    </CycleButton>
  );
}

```

## Theming

Vector components share common theming features; see [Theming Features](https://cutoff.dev/audio-ui/docs/latest/components/vector/introduction#theming-features) in the Vector introduction for an overview. The CycleButton component supports theming through the `color` and `roundness` props:

### Color Customization

```tsx title="CycleButtonColor.tsx"
import { CycleButton, OptionView } from "@cutoff/audio-ui-react";

export default function CycleButtonColorExample() {
  const [waveform, setWaveform] = useState("sine");
  return (
    <CycleButton value={waveform} onChange={(e) => setWaveform(e.value)} color="#3b82f6" label="Waveform" size="large">
      <OptionView value="sine">Sine</OptionView>
      <OptionView value="square">Square</OptionView>
    </CycleButton>
  );
}

```

### Roundness

Control the roundness of the knob ring:

```tsx title="CycleButtonRoundness.tsx"
import { CycleButton, OptionView } from "@cutoff/audio-ui-react";

export default function CycleButtonRoundnessExample() {
  const [waveform, setWaveform] = useState("sine");
  return (
    <>
      <CycleButton value={waveform} onChange={(e) => setWaveform(e.value)} roundness={0.0} label="Square" size="large">
        <OptionView value="sine">Sine</OptionView>
        <OptionView value="square">Square</OptionView>
      </CycleButton>
      <CycleButton value={waveform} onChange={(e) => setWaveform(e.value)} roundness={1.0} label="Fully Rounded" size="large">
        <OptionView value="sine">Sine</OptionView>
        <OptionView value="square">Square</OptionView>
      </CycleButton>
    </>
  );
}

```

### Thickness

Adjust the stroke thickness:

```tsx title="CycleButtonThickness.tsx"
import { useState } from "react";
import { CycleButton, OptionView } from "@cutoff/audio-ui-react";

export default function CycleButtonThicknessExample() {
  const [thinVal, setThinVal] = useState("sine");
  const [thickVal, setThickVal] = useState("sine");

  return (
    <div className="flex gap-4 items-center flex-wrap">
      <CycleButton
        value={thinVal}
        onChange={(e) => setThinVal(e.value)}
        thickness={0.2}
        label="Thin"
        size="large"
      >
        <OptionView value="sine">Sine</OptionView>
        <OptionView value="square">Square</OptionView>
      </CycleButton>
      <CycleButton
        value={thickVal}
        onChange={(e) => setThickVal(e.value)}
        thickness={0.8}
        label="Thick"
        size="large"
      >
        <OptionView value="sine">Sine</OptionView>
        <OptionView value="square">Square</OptionView>
      </CycleButton>
    </div>
  );
}
```

## Ring Configuration

### Openness

Control how much of the ring is visible:

```tsx title="CycleButtonOpenness.tsx"
import { CycleButton, OptionView } from "@cutoff/audio-ui-react";

export default function CycleButtonOpennessExample() {
  const [waveform, setWaveform] = useState("sine");
  return (
    <>
      <CycleButton value={waveform} onChange={(e) => setWaveform(e.value)} openness={0} label="Closed" size="large">
        <OptionView value="sine">Sine</OptionView>
        <OptionView value="square">Square</OptionView>
      </CycleButton>
      <CycleButton value={waveform} onChange={(e) => setWaveform(e.value)} openness={90} label="Default (90º)" size="large">
        <OptionView value="sine">Sine</OptionView>
        <OptionView value="square">Square</OptionView>
      </CycleButton>
      <CycleButton value={waveform} onChange={(e) => setWaveform(e.value)} openness={180} label="Half Circle" size="large">
        <OptionView value="sine">Sine</OptionView>
        <OptionView value="square">Square</OptionView>
      </CycleButton>
    </>
  );
}

```

### Rotation

Offset the starting angle of the ring:

```tsx title="CycleButtonRotation.tsx"
import { useState } from "react";
import { CycleButton, OptionView } from "@cutoff/audio-ui-react";

export default function CycleButtonRotationExample() {
  const [zeroVal, setZeroVal] = useState("sine");
  const [rot45Val, setRot45Val] = useState("sine");

  return (
    <div className="flex gap-4 items-center flex-wrap">
      <CycleButton
        value={zeroVal}
        onChange={(e) => setZeroVal(e.value)}
        rotation={0}
        label="No Rotation"
        size="large"
      >
        <OptionView value="sine">Sine</OptionView>
        <OptionView value="square">Square</OptionView>
      </CycleButton>
      <CycleButton
        value={rot45Val}
        onChange={(e) => setRot45Val(e.value)}
        rotation={45}
        label="45º Rotation"
        size="large"
      >
        <OptionView value="sine">Sine</OptionView>
        <OptionView value="square">Square</OptionView>
      </CycleButton>
    </div>
  );
}
```

## Adaptive Sizing

The CycleButton component supports both fixed sizes and adaptive sizing. By default, the size of the component is driven by the [Size System](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/size-system) and their `size` attribute:

```tsx title="CycleButtonSizing.tsx"
import { CycleButton, OptionView } from "@cutoff/audio-ui-react";

export default function CycleButtonSizingExample() {
  const [waveform, setWaveform] = useState("sine");
  return (
    <>
      <CycleButton value={waveform} onChange={(e) => setWaveform(e.value)} size="small" label="Small">
        <OptionView value="sine">Sine</OptionView>
        <OptionView value="square">Square</OptionView>
      </CycleButton>
      <CycleButton value={waveform} onChange={(e) => setWaveform(e.value)} size="normal" label="Normal">
        <OptionView value="sine">Sine</OptionView>
        <OptionView value="square">Square</OptionView>
      </CycleButton>
      <CycleButton value={waveform} onChange={(e) => setWaveform(e.value)} size="large" label="Large">
        <OptionView value="sine">Sine</OptionView>
        <OptionView value="square">Square</OptionView>
      </CycleButton>
      <CycleButton value={waveform} onChange={(e) => setWaveform(e.value)} size="xlarge" label="XLarge">
        <OptionView value="sine">Sine</OptionView>
        <OptionView value="square">Square</OptionView>
      </CycleButton>
    </>
  );
}

```

Adaptive sizing allows the component to adapt to the size of its parent container, adjusting its dimensions intelligently without causing distortion (`scaleToFit` display mode). The `fill` display mode makes the component fill the entire container, potentially distorting it.

```tsx title="CycleButtonAdaptiveSize.tsx"
import { useState } from "react";
import { CycleButton, OptionView } from "@cutoff/audio-ui-react";

export default function CycleButtonAdaptiveSize() {
  const [wideValue, setWideValue] = useState("sine");
  const [tallValue, setTallValue] = useState("sine");
  const [distortedValue, setDistortedValue] = useState("sine");

  return (
    <div className="flex gap-6 items-center flex-wrap">
      <div className="w-48 h-16 border rounded-lg p-2">
        <CycleButton
          value={wideValue}
          onChange={(e) => setWideValue(e.value)}
          adaptiveSize={true}
          displayMode="fill"
          label="Wide"
        >
          <OptionView value="sine">Sine</OptionView>
          <OptionView value="square">Square</OptionView>
        </CycleButton>
      </div>
      <div className="w-24 h-48 border rounded-lg p-2">
        <CycleButton
          value={tallValue}
          onChange={(e) => setTallValue(e.value)}
          adaptiveSize={true}
          displayMode="fill"
          label="Tall"
        >
          <OptionView value="sine">Sine</OptionView>
          <OptionView value="square">Square</OptionView>
        </CycleButton>
      </div>
      <div className="w-32 h-20 border rounded-lg p-2">
        <CycleButton
          value={distortedValue}
          onChange={(e) => setDistortedValue(e.value)}
          adaptiveSize={true}
          displayMode="fill"
          label="Distorted"
        >
          <OptionView value="sine">Sine</OptionView>
          <OptionView value="square">Square</OptionView>
        </CycleButton>
      </div>
    </div>
  );
}

```

## Controlled vs Uncontrolled

The CycleButton supports both controlled and uncontrolled modes:

### Controlled Mode

```tsx title="ControlledMode.tsx"
import { useState } from "react";
import { CycleButton, OptionView } from "@cutoff/audio-ui-react";

export default function ControlledModeExample() {
  const [waveform, setWaveform] = useState("sine");

  return (
    <CycleButton
      value={waveform}
      onChange={(e) => setWaveform(e.value)}
      label="Waveform"
      size="large"
    >
      <OptionView value="sine">Sine</OptionView>
      <OptionView value="square">Square</OptionView>
    </CycleButton>
  );
}
```

### Uncontrolled Mode

```tsx title="UncontrolledMode.tsx"
import { CycleButton, OptionView } from "@cutoff/audio-ui-react";

export default function UncontrolledModeExample() {
  return (
    <CycleButton
      defaultValue="sine"
      onChange={(e) => console.log("Selected:", e.value)}
      label="Waveform"
      size="large"
    >
      <OptionView value="sine">Sine</OptionView>
      <OptionView value="square">Square</OptionView>
    </CycleButton>
  );
}
```

## MIDI Integration

When using the `options` prop or parameter model, you can specify MIDI values for each option:

```tsx title="MIDIIntegration.tsx"
import { useState } from "react";
import { CycleButton } from "@cutoff/audio-ui-react";

export default function MIDIIntegrationExample() {
  const [waveform, setWaveform] = useState("sine");

  return (
    <CycleButton
      options={[
        { value: "sine", label: "Sine Wave", midiValue: 0 },
        { value: "square", label: "Square Wave", midiValue: 1 },
        { value: "sawtooth", label: "Sawtooth Wave", midiValue: 2 },
        { value: "triangle", label: "Triangle Wave", midiValue: 3 },
      ]}
      value={waveform}
      onChange={(e) => setWaveform(e.value)}
      label="Waveform"
      size="large"
    />
  );
}
```

## Common Patterns

### Waveform Selector

A typical waveform selector for synthesizers:

```tsx title="WaveformSelector.tsx"
import { useState } from "react";
import { CycleButton, OptionView } from "@cutoff/audio-ui-react";

export default function WaveformSelectorExample() {
  const [waveform, setWaveform] = useState("sine");

  return (
    <CycleButton
      value={waveform}
      onChange={(e) => setWaveform(e.value)}
      label="Waveform"
      size="large"
    >
      <OptionView value="sine"><SineWaveIcon /></OptionView>
      <OptionView value="square"><SquareWaveIcon /></OptionView>
      <OptionView value="sawtooth"><SawWaveIcon /></OptionView>
      <OptionView value="triangle"><TriangleWaveIcon /></OptionView>
    </CycleButton>
  );
}
```

### Filter Type Selector

A filter type selector:

```tsx title="FilterTypeSelector.tsx"
import { useState } from "react";
import { CycleButton, OptionView } from "@cutoff/audio-ui-react";

export default function FilterTypeSelectorExample() {
  const [filterType, setFilterType] = useState("lowpass");

  return (
    <CycleButton
      value={filterType}
      onChange={(e) => setFilterType(e.value)}
      label="Filter Type"
      size="large"
    >
      <OptionView value="lowpass">LP</OptionView>
      <OptionView value="highpass">HP</OptionView>
      <OptionView value="bandpass">BP</OptionView>
      <OptionView value="notch">Notch</OptionView>
    </CycleButton>
  );
}
```

```tsx title="CommonPatternExample.tsx"
import { useState } from "react";
import { CycleButton, OptionView } from "@cutoff/audio-ui-react";

export default function CommonPatternExample() {
  const [waveform, setWaveform] = useState("sine");
  const [filterType, setFilterType] = useState("lowpass");
  return (
    <div className="flex gap-4 items-center flex-wrap">
      <CycleButton value={waveform} onChange={(e) => setWaveform(e.value)} label="Waveform" size="large">
        <OptionView value="sine">Sine</OptionView>
        <OptionView value="square">Square</OptionView>
        <OptionView value="sawtooth">Saw</OptionView>
        <OptionView value="triangle">Tri</OptionView>
      </CycleButton>
      <CycleButton value={filterType} onChange={(e) => setFilterType(e.value)} label="Filter Type" size="large">
        <OptionView value="lowpass">LP</OptionView>
        <OptionView value="highpass">HP</OptionView>
        <OptionView value="bandpass">BP</OptionView>
        <OptionView value="notch">Notch</OptionView>
      </CycleButton>
    </div>
  );
}

```

## Common Use Cases

The Waveform Selector and Filter Type Selector examples above (in Common Patterns) show typical use cases with live demos.

## Next Steps

* Explore the [Button component](https://cutoff.dev/audio-ui/docs/latest/components/vector/button) for boolean controls
* Learn about [Knob](https://cutoff.dev/audio-ui/docs/latest/components/vector/knob) and [Slider](https://cutoff.dev/audio-ui/docs/latest/components/vector/slider) for continuous controls
* Check out [FilmStripDiscreteControl](https://cutoff.dev/audio-ui/docs/latest/components/raster/filmstrip-discrete) or [ImageRotarySwitch](https://cutoff.dev/audio-ui/docs/latest/components/raster/image-rotary-switch) for bitmap-based discrete controls
* Visit the [playground](https://playground.cutoff.dev) for interactive examples


---

# Keys

# Keys

> A responsive and interactive piano keyboard component with configurable number of keys, note highlighting, and multiple styling modes.

The Keys component provides a responsive and interactive piano keyboard visualization. It shares the same layout and accessibility as other vector components; see the [Vector introduction](https://cutoff.dev/audio-ui/docs/latest/components/vector/introduction).

* Flexible configuration: Variable number of keys (1-128), different starting positions, octave transposition
* Note highlighting: Visual feedback for active notes via `notesOn` prop
* Multiple styling modes: theme-based, classic ivory/ebony, and inverted classic
* Interactive control: Optional `onChange` callback for key press/release events
* Full theming support: color, roundness customization (in theme mode)

## Props

| Name         | Type                                                                  | Default    | Required | Description                                                                                                                             |
| :----------- | :-------------------------------------------------------------------- | :--------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------- |
| nbKeys       | number                                                                | 61         | No       | Number of keys on the keyboard (1-128)                                                                                                  |
| startKey     | "A" \| "B" \| "C" \| "D" \| "E" \| "F" \| "G"                         | -          | No       | Starting note name (A-G). Default 'C' for 61 keys, 'A' for 88 keys                                                                      |
| octaveShift  | number                                                                | 0          | No       | Octave transpose index. Positive values shift notes up by that many octaves, negative values shift down                                 |
| notesOn      | (string \| number)\[]                                                 | \[]        | No       | Array of notes that should be highlighted. Notes can be specified as strings (e.g., 'C4', 'F#5') or MIDI note numbers (e.g., 60 for C4) |
| keyStyle     | "theme" \| "classic" \| "classic-inverted"                            | theme      | No       | Key styling mode. 'theme' uses theme colors, 'classic' uses ivory/ebony colors, 'classic-inverted' inverts the classic colors           |
| onChange     | (event: AudioControlEvent<{ note: number; active: boolean }>) => void | -          | No       | Callback triggered when a key is pressed or released. Only active if this prop is provided.                                             |
| color        | string                                                                | -          | No       | Component primary color - any valid CSS color value (used in theme mode)                                                                |
| roundness    | number                                                                | -          | No       | Roundness for component corners (normalized 0.0-1.0)                                                                                    |
| adaptiveSize | boolean                                                               | false      | No       | Whether the component stretches to fill its container                                                                                   |
| size         | "xsmall" \| "small" \| "normal" \| "large" \| "xlarge"                | normal     | No       | Size of the component                                                                                                                   |
| displayMode  | "scaleToFit" \| "fill"                                                | scaleToFit | No       | Layout mode for the control                                                                                                             |
| onClick      | React.MouseEventHandler                                               | -          | No       | Click event handler                                                                                                                     |
| className    | string                                                                | -          | No       | Additional CSS classes                                                                                                                  |
| style        | React.CSSProperties                                                   | -          | No       | Additional inline styles                                                                                                                |

## Basic Usage

The Keys component can be used as a simple visualization or as an interactive control:

```tsx title="BasicKeys.tsx"
import { Keys } from "@cutoff/audio-ui-react";

export default function BasicKeys() {
  return <Keys size="large" />;
}
```

## Number of Keys

Configure the number of keys on the keyboard (1-128):

```tsx title="NumberOfKeys.tsx"
import { Keys } from "@cutoff/audio-ui-react";

export default function NumberOfKeys() {
  return (
    <div className="flex gap-4 items-center flex-wrap">
      <Keys nbKeys={25} label="25 Keys" size="large" />
      <Keys nbKeys={61} label="61 Keys (Default)" size="large" />
      <Keys nbKeys={88} label="88 Keys (Full Piano)" size="large" />
    </div>
  );
}
```

## Starting Position

Control which note the keyboard starts with:

```tsx title="StartingPosition.tsx"
import { Keys } from "@cutoff/audio-ui-react";

export default function StartingPosition() {
  return (
    <div className="flex gap-4 items-center flex-wrap">
      <Keys nbKeys={25} startKey="C" label="Starting at C" size="large" />
      <Keys nbKeys={25} startKey="A" label="Starting at A" size="large" />
      <Keys nbKeys={25} startKey="F" label="Starting at F" size="large" />
    </div>
  );
}
```

## Octave Transposition

Use the `octaveShift` prop to transpose the keyboard up or down by octaves:

```tsx title="OctaveShift.tsx"
import { Keys } from "@cutoff/audio-ui-react";

export default function OctaveShift() {
  return (
    <div className="flex gap-4 items-center flex-wrap">
      <Keys nbKeys={25} octaveShift={-2} label="2 Octaves Down" size="large" />
      <Keys nbKeys={25} octaveShift={0} label="No Shift (Default)" size="large" />
      <Keys nbKeys={25} octaveShift={2} label="2 Octaves Up" size="large" />
    </div>
  );
}
```

## Note Highlighting

Highlight active notes using the `notesOn` prop. Notes can be specified as strings (note names) or MIDI note numbers:

### Using Note Names

```tsx title="NoteNames.tsx"
import { Keys } from "@cutoff/audio-ui-react";

export default function NoteNames() {
  return (
    <Keys notesOn={["C4", "E4", "G4"]} label="C Major Chord" size="large" />
  );
}
```

### Using MIDI Note Numbers

```tsx title="MIDINotes.tsx"
import { Keys } from "@cutoff/audio-ui-react";

export default function MIDINotes() {
  return (
    <Keys notesOn={[60, 64, 67]} label="C Major Chord (MIDI)" size="large" />
  );
}
```

### Mixing Note Names and MIDI Numbers

```tsx title="MixedNotes.tsx"
import { Keys } from "@cutoff/audio-ui-react";

export default function MixedNotes() {
  return (
    <Keys notesOn={["C4", 64, "G4"]} label="Mixed Format" size="large" />
  );
}
```

### Interactive Note Highlighting

Update the highlighted notes based on user interaction:

```tsx title="InteractiveNotes.tsx"
import { useState } from "react";
import { Keys } from "@cutoff/audio-ui-react";

export default function InteractiveNotesExample() {
  const [notesOn, setNotesOn] = useState([]);

  const handleChange = (e) => {
    const noteNum = e.value.note;
    if (e.value.active) {
      setNotesOn((prev) => [...prev, noteNum]);
    } else {
      setNotesOn((prev) => prev.filter((n) => n !== noteNum));
    }
  };

  return (
    <Keys nbKeys={61} notesOn={notesOn} onChange={handleChange} size="large" />
  );
}
```

## Key Styling Modes

The Keys component supports three styling modes:

### Theme Mode

The default mode uses theme colors (from the `color` prop or global theme):

```tsx title="ThemeMode.tsx"
import { Keys } from "@cutoff/audio-ui-react";

export default function ThemeMode() {
  return (
    <Keys
      keyStyle="theme"
      color="#3b82f6"
      label="Theme Mode"
      size="large"
    />
  );
}
```

### Classic Mode

Classic piano style with ivory white keys and ebony black keys:

```tsx title="ClassicMode.tsx"
import { Keys } from "@cutoff/audio-ui-react";

export default function ClassicMode() {
  return (
    <Keys keyStyle="classic" label="Classic Piano" size="large" />
  );
}
```

### Classic Inverted Mode

Inverted classic style with ebony white keys and ivory black keys:

```tsx title="ClassicInvertedMode.tsx"
import { Keys } from "@cutoff/audio-ui-react";

export default function ClassicInvertedMode() {
  return (
    <Keys
      keyStyle="classic-inverted"
      label="Inverted Classic"
      size="large"
    />
  );
}
```

> **Note:** In classic and classic-inverted modes, active keys always use the theme color for visual feedback, regardless of the key style.

## Theming

Vector components share common theming features; see [Theming Features](https://cutoff.dev/audio-ui/docs/latest/components/vector/introduction#theming-features) in the Vector introduction for an overview. The Keys component supports theming through the `color` and `roundness` props:

### Color Customization

In theme mode, customize the color of active keys:

```tsx title="KeysColor.tsx"
import { Keys } from "@cutoff/audio-ui-react";

export default function KeysColor() {
  return (
    <div className="flex gap-4 items-center flex-wrap">
      <Keys
        keyStyle="theme"
        color="#3b82f6"
        notesOn={["C4", "E4", "G4"]}
        label="Blue Theme"
        size="large"
      />
      <Keys
        keyStyle="theme"
        color="#ef4444"
        notesOn={["C4", "E4", "G4"]}
        label="Red Theme"
        size="large"
      />
      <Keys
        keyStyle="theme"
        color="#10b981"
        notesOn={["C4", "E4", "G4"]}
        label="Green Theme"
        size="large"
      />
    </div>
  );
}
```

### Roundness

Control the roundness of the key corners:

```tsx title="KeysRoundness.tsx"
import { Keys } from "@cutoff/audio-ui-react";

export default function KeysRoundness() {
  return (
    <div className="flex gap-4 items-center flex-wrap">
      <Keys roundness={0.0} label="Square Keys" size="large" />
      <Keys roundness={0.5} label="Rounded Keys" size="large" />
      <Keys roundness={1.0} label="Fully Rounded Keys" size="large" />
    </div>
  );
}
```

## Adaptive Sizing

The Keys component supports both fixed sizes and adaptive sizing. By default, the size of the component is driven by the [Size System](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/size-system) and their `size` attribute:

```tsx title="KeysSizing.tsx"
import { Keys } from "@cutoff/audio-ui-react";

export default function KeysSizing() {
  return (
    <div className="flex gap-4 items-center flex-wrap">
      <Keys nbKeys={25} size="small" label="Small" />
      <Keys nbKeys={25} size="normal" label="Normal" />
      <Keys nbKeys={25} size="large" label="Large" />
      <Keys nbKeys={25} size="xlarge" label="XLarge" />
    </div>
  );
}
```

Adaptive sizing allows the component to adapt to the size of its parent container, adjusting its dimensions intelligently without causing distortion (`scaleToFit` display mode). The `fill` display mode makes the component fill the entire container, potentially distorting it.

## Interaction

The Keys component becomes interactive when you provide the `onChange` prop:

```tsx title="InteractiveKeys.tsx"
import { useState } from "react";
import { Keys } from "@cutoff/audio-ui-react";

export default function InteractiveKeysExample() {
  const [notesOn, setNotesOn] = useState([]);

  const handleChange = (e) => {
    const noteNum = e.value.note;
    if (e.value.active) {
      setNotesOn((prev) => [...prev, noteNum]);
    } else {
      setNotesOn((prev) => prev.filter((n) => n !== noteNum));
    }
  };

  return (
    <Keys
      nbKeys={61}
      notesOn={notesOn}
      onChange={handleChange}
      size="large"
    />
  );
}
```

The `onChange` callback receives an event with:

* `value.note`: The MIDI note number (0-127)
* `value.active`: Whether the note is being pressed (true) or released (false)
* `normalizedValue`: The normalized value (0.0-1.0)
* `midiValue`: The MIDI note number

## Common Patterns

### Full Piano Keyboard

A standard 88-key piano keyboard:

```tsx title="FullPiano.tsx"
import { Keys } from "@cutoff/audio-ui-react";

export default function FullPiano() {
  return (
    <Keys nbKeys={88} startKey="A" label="Full Piano" size="large" />
  );
}
```

### Compact Keyboard

A compact 25-key keyboard for mobile or space-constrained interfaces:

```tsx title="CompactKeyboard.tsx"
import { Keys } from "@cutoff/audio-ui-react";

export default function CompactKeyboard() {
  return (
    <Keys nbKeys={25} startKey="C" label="Compact" size="large" />
  );
}
```

### MIDI Monitor

A keyboard that highlights notes received from MIDI input. In a real app you would connect this to MIDI input; here it shows a static example:

```tsx title="MIDIMonitor.tsx"
import { useState } from "react";
import { Keys } from "@cutoff/audio-ui-react";

export default function MIDIMonitorExample() {
  const [notesOn, setNotesOn] = useState([]);

  return (
    <Keys
      nbKeys={61}
      notesOn={notesOn}
      label="MIDI Monitor"
      size="large"
    />
  );
}
```

### Chord Display

Display a chord by highlighting specific notes:

```tsx title="ChordDisplay.tsx"
import { useState } from "react";
import { Keys } from "@cutoff/audio-ui-react";

const C_MAJOR = ["C4", "E4", "G4"];
const A_MINOR = ["A3", "C4", "E4"];
const F_MAJOR = ["F3", "A3", "C4"];

export default function ChordDisplayExample() {
  const [chord, setChord] = useState(C_MAJOR);

  return (
    <div className="flex flex-col gap-4">
      <div className="flex gap-2">
        <button
          type="button"
          onClick={() => setChord(C_MAJOR)}
          className="rounded border px-2 py-1 text-sm"
        >
          C Major
        </button>
        <button
          type="button"
          onClick={() => setChord(A_MINOR)}
          className="rounded border px-2 py-1 text-sm"
        >
          A Minor
        </button>
        <button
          type="button"
          onClick={() => setChord(F_MAJOR)}
          className="rounded border px-2 py-1 text-sm"
        >
          F Major
        </button>
      </div>
      <Keys nbKeys={61} notesOn={chord} label="Chord Display" size="large" />
    </div>
  );
}
```

## Layout Considerations

The Keys component maintains proper piano key layout and proportions regardless of the number of keys or starting position. The component:

* Automatically calculates white and black key positions
* Maintains correct spacing between keys
* Handles partial octaves correctly
* Supports any number of keys from 1 to 128

The component uses SVG for rendering, ensuring crisp display at any size and proper scaling.

## Common Use Cases

### MIDI Keyboard Visualization

```tsx title="MIDIKeyboard.tsx"
import { useState } from "react";
import { Keys } from "@cutoff/audio-ui-react";

export default function MIDIKeyboardExample() {
  const [activeNotes, setActiveNotes] = useState([]);

  const handleNoteChange = (e) => {
    if (e.value.active) {
      setActiveNotes((prev) => [...prev, e.value.note]);
    } else {
      setActiveNotes((prev) => prev.filter((n) => n !== e.value.note));
    }
  };

  return (
    <Keys
      nbKeys={61}
      notesOn={activeNotes}
      onChange={handleNoteChange}
      keyStyle="theme"
      size="large"
    />
  );
}
```

### Piano Roll Display

```tsx title="PianoRoll.tsx"
import { Keys } from "@cutoff/audio-ui-react";

export default function PianoRollExample() {
  const playedNotes = [60, 64, 67];

  return (
    <Keys
      nbKeys={88}
      startKey="A"
      notesOn={playedNotes}
      keyStyle="classic"
      size="large"
    />
  );
}
```

### Compact Keyboard Control

```tsx title="CompactKeyboardControl.tsx"
import { useState } from "react";
import { Keys } from "@cutoff/audio-ui-react";

export default function CompactKeyboardControlExample() {
  const [notesOn, setNotesOn] = useState([]);

  const handleNoteChange = (e) => {
    if (e.value.active) {
      setNotesOn((prev) => [...prev, e.value.note]);
    } else {
      setNotesOn((prev) => prev.filter((n) => n !== e.value.note));
    }
  };

  return (
    <Keys
      nbKeys={25}
      startKey="C"
      octaveShift={-1}
      onChange={handleNoteChange}
      size="large"
    />
  );
}
```

## Next Steps

* Explore other [vector components](https://cutoff.dev/audio-ui/docs/latest/components/vector) like [Knob](https://cutoff.dev/audio-ui/docs/latest/components/vector/knob) and [Slider](https://cutoff.dev/audio-ui/docs/latest/components/vector/slider)
* Learn about [raster components](https://cutoff.dev/audio-ui/docs/latest/components/raster) for bitmap-based controls
* Visit the [playground](https://playground.cutoff.dev) for interactive examples


---

# Introduction

# Raster Components

> Overview of bitmap-based controls: film strip (sprite sheet) components and image-based components, with guidelines for asset usage.

Raster components use bitmap graphics instead of SVG to render controls. They fall into two categories: **film strip** controls (sprite sheets) and **image** controls (single or paired bitmaps). Use them when you need a fixed visual style, assets from VST tools, or bitmap-only designs.

Because their appearance comes from bitmap assets, raster components are not themable like the built-in vector components. They do not respond to theme changes (e.g. primary color, roundness). They do benefit from the rest of AudioUI: the sizing system, interaction system, audio parameter model, layout, and accessibility all apply.

> **Note:** AudioUI will provide props to supply separate assets for light and dark mode. (Feature coming soon.)

## Two Categories

### Film Strip Components

Film strip controls display one frame at a time from a **sprite sheet**: a single image file where multiple frames are stacked vertically or horizontally. Each frame corresponds to a value state (continuous position, discrete option, or boolean on/off).

* [FilmStripContinuousControl](https://cutoff.dev/audio-ui/docs/latest/components/raster/filmstrip-continuous): one frame per value along a continuous range (e.g. VU meter, rotary strip).
* [FilmStripDiscreteControl](https://cutoff.dev/audio-ui/docs/latest/components/raster/filmstrip-discrete): one frame per discrete option (e.g. multi-position switch, waveform strip).
* [FilmStripBooleanControl](https://cutoff.dev/audio-ui/docs/latest/components/raster/filmstrip-boolean): two frames for off/on (e.g. power switch, momentary button).

### Image Components

Image controls use one or two bitmap images that are rotated or swapped by value. They do not use sprite sheets.

* [ImageKnob](https://cutoff.dev/audio-ui/docs/latest/components/raster/image-knob): one image rotated by a continuous value.
* [ImageRotarySwitch](https://cutoff.dev/audio-ui/docs/latest/components/raster/image-rotary-switch): one image rotated to discrete positions.
* [ImageSwitch](https://cutoff.dev/audio-ui/docs/latest/components/raster/image-switch): two images (off/on) swapped by a boolean value.

## Film Strips (Sprite Sheets)

A film strip is a bitmap where every frame has the same size and frames are arranged in a single row or column. This format is an **industry standard** for VST and audio plugin GUIs: many plugins and authoring tools export controls as sprite sheets so the host or UI can show the correct frame for the current parameter value.

The format is widely supported by tools such as [WebKnobMan](https://www.g200kg.com/en/webknobman/), which lets you design knobs and switches and export them as PNG (or other bitmap) sprite sheets. You can also find ready-made assets in the [WebKnobMan gallery](https://www.g200kg.com/en/webknobman/gallery.php), where users share knob and strip designs; from there you can download project files and use the EasyRendering function to export images with the frame size and count you need.

## Guidelines for Film Strip Assets

Frame dimensions, frame count, and orientation are **fixed by the asset**. They are determined when the image is created (e.g. in WebKnobMan or another tool). Do not change them in code to match a different number of options or a different layout; that will break the display.

* **Frame dimensions**: `frameWidth` and `frameHeight` must match the width and height of one frame in the sprite sheet. If the asset has 120×80 pixel frames, use `frameWidth={120}` and `frameHeight={80}`.
* **Frame count**: `frameCount` must equal the total number of frames in the image. For a boolean strip that is 2 frames, use `frameCount={2}`. For a discrete strip with 8 states, use `frameCount={8}`. The number of options (or value range) in your component must match this count where applicable (e.g. discrete control options map 1:1 to frames).
* **Orientation**: Frames are either stacked vertically (top to bottom) or horizontally (left to right). Set `orientation` to match how your sprite sheet is laid out. Default is `"vertical"`.
* **One asset, one configuration**: When you use a given film strip image, use the same `frameWidth`, `frameHeight`, `frameCount`, and `orientation` everywhere that image is used. Do not reuse the same image with different frame counts or dimensions for different examples or screens.

The look of each raster control is defined entirely by its bitmap; for theme-driven styling (color, roundness, etc.), use the [vector components](https://cutoff.dev/audio-ui/docs/latest/components/vector/introduction) instead and see [Theming Features](https://cutoff.dev/audio-ui/docs/latest/components/vector/introduction#theming-features).

## Performance

Raster components follow the same performance priorities as the rest of AudioUI; for the overall approach, see [Introduction](https://cutoff.dev/audio-ui/docs/latest/getting-started/introduction) (Performance First). Bitmap-based rendering also has specific advantages in dense UIs.

* **Bitmap compositing**: The browser draws a region of a decoded image (or sprite sheet) instead of recalculating vector paths. Updating the displayed frame or rotation is a cheap operation (e.g. changing which region is visible or applying a transform), so value changes do not trigger full redraws.
* **Image reuse**: Controls that use the same `imageHref` (or the same film strip) share one decoded bitmap. The browser decodes and caches the image once; multiple instances on the page reuse that resource. This helps when you have many knobs or switches from the same asset.
* **Film strips**: A single sprite sheet holds all frames. One network request and one decode serve every frame; switching value only changes which part of the image is shown. That keeps memory and draw cost predictable even with many discrete or continuous raster controls.
* **Re-renders**: As with other AudioUI components, raster controls re-render only when their props or value change. The interaction system uses refs for high-frequency state during drag or wheel so that the rest of the tree is not updated on every move.

For UIs with many identical-looking controls (e.g. a mixer strip of film strip faders or image knobs), raster components can be a good fit: one asset, one decode, and cheap updates per control.

## Completeness and roadmap

Raster components are basically complete: all variants of film strip and image-based controls are covered.

## Next Steps

* Use [FilmStripContinuousControl](https://cutoff.dev/audio-ui/docs/latest/components/raster/filmstrip-continuous), [FilmStripDiscreteControl](https://cutoff.dev/audio-ui/docs/latest/components/raster/filmstrip-discrete), or [FilmStripBooleanControl](https://cutoff.dev/audio-ui/docs/latest/components/raster/filmstrip-boolean) for sprite sheet-based controls.
* Use [ImageKnob](https://cutoff.dev/audio-ui/docs/latest/components/raster/image-knob), [ImageRotarySwitch](https://cutoff.dev/audio-ui/docs/latest/components/raster/image-rotary-switch), or [ImageSwitch](https://cutoff.dev/audio-ui/docs/latest/components/raster/image-switch) for single- or dual-image controls.
* Check the [playground](https://playground.cutoff.dev) for interactive raster examples.


---

# FilmStripContinuousControl

# FilmStripContinuousControl

> A continuous control that displays frames from a filmstrip sprite sheet, supporting the industry-standard bitmap-based control representation.

FilmStripContinuousControl is a continuous control that displays frames from a filmstrip sprite sheet. It supports the widely-used industry standard for control representation: bitmap sprite sheets (filmstrips).

While bitmap-based visualization is more constrained than SVG (no dynamic theming, fixed visual appearance), this component shares the same layout, parameter model, interaction, and accessibility as other controls (vector, raster, and control primitives); see the [Raster introduction](https://cutoff.dev/audio-ui/docs/latest/components/raster/introduction).

The frame displayed is determined by the normalized value (0-1 maps to frame 0 to frameCount-1).

> **Note:** **No Theming Support**: This component does NOT support themable props (color, roundness, thickness) as visuals are entirely determined by the image content.

## Props

| Name                   | Type                                                                 | Default   | Required | Description                                                                                                                         |
| :--------------------- | :------------------------------------------------------------------- | :-------- | :------- | :---------------------------------------------------------------------------------------------------------------------------------- |
| value                  | number                                                               | -         | Yes      | Current value of the control                                                                                                        |
| onChange               | (event: AudioControlEvent\<number>) => void                          | -         | No       | Handler for value changes                                                                                                           |
| min                    | number                                                               | -         | No       | Minimum value (ad-hoc mode)                                                                                                         |
| max                    | number                                                               | -         | No       | Maximum value (ad-hoc mode)                                                                                                         |
| step                   | number                                                               | -         | No       | Step size for value adjustments                                                                                                     |
| bipolar                | boolean                                                              | false     | No       | Whether the component should operate in bipolar mode (centered at zero)                                                             |
| label                  | string                                                               | -         | No       | Label displayed below the component                                                                                                 |
| frameWidth             | number                                                               | -         | Yes      | Width of a single frame in the filmstrip (pixels)                                                                                   |
| frameHeight            | number                                                               | -         | Yes      | Height of a single frame in the filmstrip (pixels)                                                                                  |
| frameCount             | number                                                               | -         | Yes      | Total number of frames in the filmstrip sprite sheet                                                                                |
| imageHref              | string                                                               | -         | Yes      | URL to the sprite sheet/filmstrip image                                                                                             |
| orientation            | "vertical" \| "horizontal"                                           | vertical  | No       | Orientation of the filmstrip (frames stacked vertically or horizontally)                                                            |
| frameRotation          | number                                                               | 0         | No       | Optional frame rotation in degrees                                                                                                  |
| invertValue            | boolean                                                              | false     | No       | If true, inverts the normalized value (0.0 -> 1.0 and 1.0 -> 0.0). Useful for filmstrips where frame 0 represents the maximum value |
| valueAsLabel           | "labelOnly" \| "valueOnly" \| "interactive"                          | labelOnly | No       | Controls how the label and value are displayed                                                                                      |
| adaptiveSize           | boolean                                                              | false     | No       | Whether the component stretches to fill its container                                                                               |
| size                   | "xsmall" \| "small" \| "normal" \| "large" \| "xlarge"               | normal    | No       | Size of the component                                                                                                               |
| parameter              | ContinuousParameter                                                  | -         | No       | Audio Parameter definition (Model). If provided, overrides min/max/step/label/bipolar from ad-hoc props                             |
| valueFormatter         | (value: number, parameterDef: AudioParameter) => string \| undefined | -         | No       | Custom renderer for the value display                                                                                               |
| interactionMode        | "drag" \| "wheel" \| "both"                                          | both      | No       | Interaction mode                                                                                                                    |
| interactionDirection   | "vertical" \| "horizontal" \| "circular" \| "both"                   | -         | No       | Direction of interaction                                                                                                            |
| interactionSensitivity | number                                                               | -         | No       | Sensitivity multiplier for interactions (default varies by control type)                                                            |

## Basic Usage

FilmStripContinuousControl requires a filmstrip sprite sheet image and frame dimensions:

```tsx title="BasicFilmStrip.tsx"
import { useState } from "react";
import { FilmStripContinuousControl } from "@cutoff/audio-ui-react";

export default function BasicFilmStripExample() {
  const [value, setValue] = useState(50);

  return (
    <FilmStripContinuousControl
      value={value}
      min={0}
      max={100}
      label="VU Meter"
      size="large"
      frameWidth={120}
      frameHeight={80}
      frameCount={48}
      imageHref="/images/demo/vu3.png"
      onChange={(e) => setValue(e.value)}
    />
  );
}
```

## Filmstrip Configuration

Filmstrips are bitmap sprite sheets containing multiple frames stacked vertically (default) or horizontally. Each frame represents a different value state.

### Frame Dimensions

Specify the dimensions of a single frame:

```tsx title="FrameDimensions.tsx"
import { useState } from "react";
import { FilmStripContinuousControl } from "@cutoff/audio-ui-react";

export default function FrameDimensionsExample() {
  const [value, setValue] = useState(50);

  return (
    <FilmStripContinuousControl
      value={value}
      min={0}
      max={100}
      label="VU Meter"
      size="large"
      frameWidth={120}
      frameHeight={80}
      frameCount={48}
      imageHref="/images/demo/vu3.png"
      onChange={(e) => setValue(e.value)}
    />
  );
}
```

### Orientation

Control whether frames are stacked vertically or horizontally:

```tsx title="Orientation.tsx"
import { useState } from "react";
import { FilmStripContinuousControl } from "@cutoff/audio-ui-react";

export default function OrientationExample() {
  const [verticalVal, setVerticalVal] = useState(50);
  const [horizontalVal, setHorizontalVal] = useState(50);

  return (
    <div className="flex gap-4 items-center flex-wrap">
      <FilmStripContinuousControl
        value={verticalVal}
        min={0}
        max={100}
        label="Vertical"
        size="large"
        frameWidth={120}
        frameHeight={80}
        frameCount={48}
        imageHref="/images/demo/vu3.png"
        orientation="vertical"
        onChange={(e) => setVerticalVal(e.value)}
      />
      <FilmStripContinuousControl
        value={horizontalVal}
        min={0}
        max={100}
        label="Horizontal"
        size="large"
        frameWidth={120}
        frameHeight={80}
        frameCount={48}
        imageHref="/images/demo/vu3.png"
        orientation="horizontal"
        onChange={(e) => setHorizontalVal(e.value)}
      />
    </div>
  );
}
```

### Frame Rotation

Rotate the entire filmstrip container:

```tsx title="FrameRotation.tsx"
import { useState } from "react";
import { FilmStripContinuousControl } from "@cutoff/audio-ui-react";

export default function FrameRotationExample() {
  const [value, setValue] = useState(50);

  return (
    <FilmStripContinuousControl
      value={value}
      min={0}
      max={100}
      label="Rotated"
      size="large"
      frameWidth={120}
      frameHeight={80}
      frameCount={48}
      imageHref="/images/demo/vu3.png"
      frameRotation={45}
      onChange={(e) => setValue(e.value)}
    />
  );
}
```

### Value Inversion

Invert the value mapping for filmstrips where frame 0 represents the maximum value:

```tsx title="ValueInversion.tsx"
import { useState } from "react";
import { FilmStripContinuousControl } from "@cutoff/audio-ui-react";

export default function ValueInversionExample() {
  const [value, setValue] = useState(50);

  return (
    <FilmStripContinuousControl
      value={value}
      min={0}
      max={100}
      label="Inverted"
      size="large"
      frameWidth={120}
      frameHeight={80}
      frameCount={48}
      imageHref="/images/demo/vu3.png"
      invertValue={true}
      onChange={(e) => setValue(e.value)}
    />
  );
}
```

## Value Mapping

The component maps continuous values (0-1 normalized) to frame indices (0 to frameCount-1):

* Value 0.0 (min) → Frame 0
* Value 1.0 (max) → Frame (frameCount - 1)
* Intermediate values are linearly interpolated

## Parameter Model

Use a parameter model for integration with audio parameter systems. The following example uses ad-hoc props; with the full library you would use `createContinuousParameter` and the `parameter` prop:

```tsx title="FilmStripParameterModel.tsx"
import { useState } from "react";
import { FilmStripContinuousControl } from "@cutoff/audio-ui-react";

export default function FilmStripParameterModelExample() {
  const [value, setValue] = useState(50);

  return (
    <FilmStripContinuousControl
      min={0}
      max={100}
      label="Volume"
      value={value}
      size="large"
      frameWidth={120}
      frameHeight={80}
      frameCount={48}
      imageHref="/images/demo/vu3.png"
      onChange={(e) => setValue(e.value)}
    />
  );
}
```

## Adaptive Sizing

The FilmStripContinuousControl component supports both fixed sizes and adaptive sizing. By default, the size of the component is driven by the [Size System](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/size-system) and their `size` attribute:

```tsx title="FilmStripSizing.tsx"
import { useState } from "react";
import { FilmStripContinuousControl } from "@cutoff/audio-ui-react";

export default function FilmStripSizingExample() {
  const [v1, setV1] = useState(50);
  const [v2, setV2] = useState(50);
  const [v3, setV3] = useState(50);
  const [v4, setV4] = useState(50);

  return (
    <div className="flex gap-4 items-center flex-wrap">
      <FilmStripContinuousControl
        value={v1}
        min={0}
        max={100}
        label="Small"
        size="small"
        frameWidth={120}
        frameHeight={80}
        frameCount={48}
        imageHref="/images/demo/vu3.png"
        onChange={(e) => setV1(e.value)}
      />
      <FilmStripContinuousControl
        value={v2}
        min={0}
        max={100}
        label="Normal"
        size="normal"
        frameWidth={120}
        frameHeight={80}
        frameCount={48}
        imageHref="/images/demo/vu3.png"
        onChange={(e) => setV2(e.value)}
      />
      <FilmStripContinuousControl
        value={v3}
        min={0}
        max={100}
        label="Large"
        size="large"
        frameWidth={120}
        frameHeight={80}
        frameCount={48}
        imageHref="/images/demo/vu3.png"
        onChange={(e) => setV3(e.value)}
      />
      <FilmStripContinuousControl
        value={v4}
        min={0}
        max={100}
        label="XLarge"
        size="xlarge"
        frameWidth={120}
        frameHeight={80}
        frameCount={48}
        imageHref="/images/demo/vu3.png"
        onChange={(e) => setV4(e.value)}
      />
    </div>
  );
}
```

Adaptive sizing allows the component to adapt to the size of its parent container, adjusting its dimensions intelligently without causing distortion (`scaleToFit` display mode). The `fill` display mode makes the component fill the entire container, potentially distorting it.

```tsx title="FilmStripContinuousAdaptiveSize.tsx"
import { useState } from "react";
import { FilmStripContinuousControl } from "@cutoff/audio-ui-react";

export default function FilmStripContinuousAdaptiveSize() {
  const [wideVal, setWideVal] = useState(50);
  const [tallVal, setTallVal] = useState(50);
  const [distortedVal, setDistortedVal] = useState(50);

  return (
    <div className="flex gap-6 items-center flex-wrap">
      <div className="w-48 h-12 border rounded-lg p-2">
        <FilmStripContinuousControl
          value={wideVal}
          min={0}
          max={100}
          onChange={(e) => setWideVal(e.value)}
          adaptiveSize={true}
          label="Wide"
          frameWidth={120}
          frameHeight={80}
          frameCount={48}
          imageHref="/images/demo/vu3.png"
        />
      </div>
      <div className="w-20 h-32 border rounded-lg p-2">
        <FilmStripContinuousControl
          value={tallVal}
          min={0}
          max={100}
          onChange={(e) => setTallVal(e.value)}
          adaptiveSize={true}
          label="Tall"
          frameWidth={120}
          frameHeight={80}
          frameCount={48}
          imageHref="/images/demo/vu3.png"
        />
      </div>
      <div className="w-32 h-20 border rounded-lg p-2">
        <FilmStripContinuousControl
          value={distortedVal}
          min={0}
          max={100}
          onChange={(e) => setDistortedVal(e.value)}
          adaptiveSize={true}
          displayMode="fill"
          label="Distorted"
          frameWidth={120}
          frameHeight={80}
          frameCount={48}
          imageHref="/images/demo/vu3.png"
        />
      </div>
    </div>
  );
}
```

## Interaction Modes

Control how users interact with the filmstrip:

```tsx title="FilmStripInteraction.tsx"
import { useState } from "react";
import { FilmStripContinuousControl } from "@cutoff/audio-ui-react";

export default function FilmStripInteractionExample() {
  const [dragVal, setDragVal] = useState(50);
  const [wheelVal, setWheelVal] = useState(50);

  return (
    <div className="flex gap-4 items-center flex-wrap">
      <FilmStripContinuousControl
        value={dragVal}
        min={0}
        max={100}
        label="Drag Only"
        interactionMode="drag"
        size="large"
        frameWidth={120}
        frameHeight={80}
        frameCount={48}
        imageHref="/images/demo/vu3.png"
        onChange={(e) => setDragVal(e.value)}
      />
      <FilmStripContinuousControl
        value={wheelVal}
        min={0}
        max={100}
        label="Wheel Only"
        interactionMode="wheel"
        size="large"
        frameWidth={120}
        frameHeight={80}
        frameCount={48}
        imageHref="/images/demo/vu3.png"
        onChange={(e) => setWheelVal(e.value)}
      />
    </div>
  );
}
```

## Common Use Cases

### VU Meter

```tsx title="VUMeter.tsx"
import { useState } from "react";
import { FilmStripContinuousControl } from "@cutoff/audio-ui-react";

export default function VUMeterExample() {
  const [value, setValue] = useState(50);

  return (
    <FilmStripContinuousControl
      value={value}
      min={0}
      max={100}
      label="VU Meter"
      size="large"
      frameWidth={120}
      frameHeight={80}
      frameCount={48}
      imageHref="/images/demo/vu3.png"
      valueAsLabel="interactive"
      onChange={(e) => setValue(e.value)}
    />
  );
}
```

### Rotary Knob

```tsx title="RotaryKnob.tsx"
import { useState } from "react";
import { FilmStripContinuousControl } from "@cutoff/audio-ui-react";

export default function RotaryKnobExample() {
  const [value, setValue] = useState(5);

  return (
    <FilmStripContinuousControl
      value={value}
      min={1}
      max={10}
      label="Tone"
      size="large"
      frameWidth={120}
      frameHeight={80}
      frameCount={48}
      imageHref="/images/demo/vu3.png"
      interactionDirection="circular"
      valueAsLabel="interactive"
      onChange={(e) => setValue(e.value)}
    />
  );
}
```

## Filmstrip Image Format

Filmstrip images should follow these conventions:

* **Vertical orientation (default)**: Frames stacked top to bottom
* **Horizontal orientation**: Frames stacked left to right
* **Frame dimensions**: All frames must have identical dimensions
* **Frame count**: Total number of frames in the sprite sheet
* **Compatibility**: Works with filmstrips exported from WebKnobMan and other VST development tools

## Next Steps

* Explore [FilmStripDiscreteControl](https://cutoff.dev/audio-ui/docs/latest/components/raster/filmstrip-discrete) for discrete parameter control
* Learn about [FilmStripBooleanControl](https://cutoff.dev/audio-ui/docs/latest/components/raster/filmstrip-boolean) for boolean switches
* Check out the [playground](https://playground.cutoff.dev) for interactive examples


---

# FilmStripDiscreteControl

# FilmStripDiscreteControl

> A discrete control that displays frames from a filmstrip sprite sheet, supporting the industry-standard bitmap-based control representation.

FilmStripDiscreteControl is a discrete control that displays frames from a filmstrip sprite sheet. It supports the widely-used industry standard for control representation: bitmap sprite sheets (filmstrips).

While bitmap-based visualization is more constrained than SVG (no dynamic theming, fixed visual appearance), this component shares the same layout, parameter model, interaction, and accessibility as other controls (vector, raster, and control primitives); see the [Raster introduction](https://cutoff.dev/audio-ui/docs/latest/components/raster/introduction).

The frame displayed is determined by mapping the current value to a frame index based on the number of options.

> **Note:** **No Theming Support**: This component does NOT support themable props (color, roundness, thickness) as visuals are entirely determined by the image content.

## Props

| Name          | Type                                                                   | Default  | Required | Description                                                                                |
| :------------ | :--------------------------------------------------------------------- | :------- | :------- | :----------------------------------------------------------------------------------------- |
| value         | string \| number                                                       | -        | Yes      | Current value of the control                                                               |
| onChange      | (event: AudioControlEvent\<string \| number>) => void                  | -        | No       | Handler for value changes                                                                  |
| label         | string                                                                 | -        | No       | Label displayed below the component                                                        |
| frameWidth    | number                                                                 | -        | Yes      | Width of a single frame in the filmstrip (pixels)                                          |
| frameHeight   | number                                                                 | -        | Yes      | Height of a single frame in the filmstrip (pixels)                                         |
| frameCount    | number                                                                 | -        | Yes      | Total number of frames in the filmstrip sprite sheet                                       |
| imageHref     | string                                                                 | -        | Yes      | URL to the sprite sheet/filmstrip image                                                    |
| orientation   | "vertical" \| "horizontal"                                             | vertical | No       | Orientation of the filmstrip (frames stacked vertically or horizontally)                   |
| frameRotation | number                                                                 | 0        | No       | Optional frame rotation in degrees                                                         |
| adaptiveSize  | boolean                                                                | false    | No       | Whether the component stretches to fill its container                                      |
| size          | "xsmall" \| "small" \| "normal" \| "large" \| "xlarge"                 | normal   | No       | Size of the component                                                                      |
| parameter     | DiscreteParameter                                                      | -        | No       | Audio Parameter definition (Model). If provided, overrides options/label from ad-hoc props |
| options       | Array<{ value: string \| number; label?: string; midiValue?: number }> | -        | No       | Array of option definitions (strict mode)                                                  |
| children      | React.ReactNode                                                        | -        | No       | OptionView children for ad-hoc or hybrid mode                                              |

## Understanding Options vs Children

FilmStripDiscreteControl distinguishes between two concepts:

* **`options` prop**: Defines the parameter model (value, label, midiValue). Used for parameter structure.
* **`children` (OptionView components)**: In this component, discrete visuals are handled by the filmstrip image (each frame), not by OptionView content. OptionView here is mainly syntactic sugar: it populates the option set in ad-hoc mode (values and count); the component uses the number of OptionView children to derive frame index. The visual content inside OptionView (e.g. text or icons) is never displayed.

These serve different purposes and can be used together: use `options` or a `parameter` when you have data-driven option definitions or need MIDI mapping; use OptionView children for simple, ad-hoc setups when you want the option set inferred from the children.

> **Note:** **Basic use**: For basic use of FilmStripDiscreteControl, and especially when using ad-hoc props (no `parameter` prop), prefer **OptionView** children. In that case the option set is inferred from the OptionViews and the frame count should match the number of options. Use the `options` prop or a full parameter when you need explicit parameter structure (e.g. data-driven options, MIDI mapping). For a full disambiguation of options vs OptionView, see [Options vs OptionView](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/options-and-optionview).

## Basic Usage

FilmStripDiscreteControl requires a filmstrip sprite sheet image and frame dimensions. Use OptionView children to define the discrete options:

```tsx title="BasicFilmStripDiscrete.tsx"
import { useState } from "react";
import { FilmStripDiscreteControl, OptionView } from "@cutoff/audio-ui-react";

export default function BasicFilmStripDiscreteExample() {
  const [value, setValue] = useState(4);

  return (
    <FilmStripDiscreteControl
      value={value}
      label="Traffic Light"
      size="large"
      frameWidth={103}
      frameHeight={260}
      frameCount={8}
      imageHref="/images/demo/traffic-light-filmstrip.png"
      onChange={(e) => setValue(e.value)}
    >
      {[1, 2, 3, 4, 5, 6, 7, 8].map((n) => (
        <OptionView key={n} value={n}>State {n}</OptionView>
      ))}
    </FilmStripDiscreteControl>
  );
}
```

## Modes of Operation

FilmStripDiscreteControl supports three modes of operation:

### Ad-Hoc Mode (Children Only)

Model inferred from OptionView children:

```tsx title="AdHocFilmStripDiscrete.tsx"
import { useState } from "react";
import { FilmStripDiscreteControl, OptionView } from "@cutoff/audio-ui-react";

export default function AdHocFilmStripDiscreteExample() {
  const [value, setValue] = useState(4);
  const options = [1, 2, 3, 4, 5, 6, 7, 8];

  return (
    <FilmStripDiscreteControl
      value={value}
      label="Traffic Light"
      size="large"
      frameWidth={103}
      frameHeight={260}
      frameCount={8}
      imageHref="/images/demo/traffic-light-filmstrip.png"
      onChange={(e) => setValue(e.value)}
    >
      {options.map((n) => (
        <OptionView key={n} value={n}>State {n}</OptionView>
      ))}
    </FilmStripDiscreteControl>
  );
}
```

### Strict Mode (Parameter Only)

Model provided via parameter prop. The following example uses children (OptionView); with the full library you would use `createDiscreteParameter` and the `parameter` prop:

```tsx title="StrictModeFilmStripDiscrete.tsx"
import { useState } from "react";
import { FilmStripDiscreteControl, OptionView } from "@cutoff/audio-ui-react";

export default function StrictModeFilmStripDiscreteExample() {
  const [value, setValue] = useState(4);
  const options = [1, 2, 3, 4, 5, 6, 7, 8];

  return (
    <FilmStripDiscreteControl
      value={value}
      label="Traffic Light"
      size="large"
      frameWidth={103}
      frameHeight={260}
      frameCount={8}
      imageHref="/images/demo/traffic-light-filmstrip.png"
      onChange={(e) => setValue(e.value)}
    >
      {options.map((n) => (
        <OptionView key={n} value={n}>State {n}</OptionView>
      ))}
    </FilmStripDiscreteControl>
  );
}
```

### Hybrid Mode (Parameter + Children)

Model from parameter, View from children (matched by value). Use the same pattern as Strict Mode with OptionView children when the parameter is not in scope.

```tsx title="HybridModeFilmStripDiscrete.tsx"
import { useState } from "react";
import { FilmStripDiscreteControl, OptionView } from "@cutoff/audio-ui-react";

export default function HybridModeFilmStripDiscreteExample() {
  const [value, setValue] = useState(4);
  const options = [1, 2, 3, 4, 5, 6, 7, 8];

  return (
    <FilmStripDiscreteControl
      value={value}
      label="Traffic Light"
      size="large"
      frameWidth={103}
      frameHeight={260}
      frameCount={8}
      imageHref="/images/demo/traffic-light-filmstrip.png"
      onChange={(e) => setValue(e.value)}
    >
      {options.map((n) => (
        <OptionView key={n} value={n}>State {n}</OptionView>
      ))}
    </FilmStripDiscreteControl>
  );
}
```

## Frame Mapping

The component maps discrete option values to frame indices:

* Option index 0 → Frame 0
* Option index 1 → Frame 1
* Option index N → Frame N

The number of frames in the filmstrip should match the number of options.

## Filmstrip Configuration

### Frame Dimensions

Specify the dimensions of a single frame:

```tsx title="FrameDimensions.tsx"
import { useState } from "react";
import { FilmStripDiscreteControl, OptionView } from "@cutoff/audio-ui-react";

export default function FrameDimensionsExample() {
  const [value, setValue] = useState(4);
  const options = [1, 2, 3, 4, 5, 6, 7, 8];

  return (
    <FilmStripDiscreteControl
      value={value}
      label="Traffic Light"
      size="large"
      frameWidth={103}
      frameHeight={260}
      frameCount={8}
      imageHref="/images/demo/traffic-light-filmstrip.png"
      onChange={(e) => setValue(e.value)}
    >
      {options.map((n) => (
        <OptionView key={n} value={n}>State {n}</OptionView>
      ))}
    </FilmStripDiscreteControl>
  );
}
```

### Orientation

Control whether frames are stacked vertically or horizontally:

```tsx title="Orientation.tsx"
import { useState } from "react";
import { FilmStripDiscreteControl, OptionView } from "@cutoff/audio-ui-react";

export default function OrientationExample() {
  const [value, setValue] = useState(4);
  const options = [1, 2, 3, 4, 5, 6, 7, 8];

  return (
    <FilmStripDiscreteControl
      value={value}
      label="Traffic Light"
      size="large"
      frameWidth={103}
      frameHeight={260}
      frameCount={8}
      imageHref="/images/demo/traffic-light-filmstrip.png"
      orientation="vertical"
      onChange={(e) => setValue(e.value)}
    >
      {options.map((n) => (
        <OptionView key={n} value={n}>State {n}</OptionView>
      ))}
    </FilmStripDiscreteControl>
  );
}
```

### Frame Rotation

Rotate the entire filmstrip container:

```tsx title="FrameRotation.tsx"
import { useState } from "react";
import { FilmStripDiscreteControl, OptionView } from "@cutoff/audio-ui-react";

export default function FrameRotationExample() {
  const [value, setValue] = useState(4);
  const options = [1, 2, 3, 4, 5, 6, 7, 8];

  return (
    <FilmStripDiscreteControl
      value={value}
      label="Traffic Light"
      size="large"
      frameWidth={103}
      frameHeight={260}
      frameCount={8}
      imageHref="/images/demo/traffic-light-filmstrip.png"
      frameRotation={90}
      onChange={(e) => setValue(e.value)}
    >
      {options.map((n) => (
        <OptionView key={n} value={n}>State {n}</OptionView>
      ))}
    </FilmStripDiscreteControl>
  );
}
```

## Adaptive Sizing

The FilmStripDiscreteControl component supports both fixed sizes and adaptive sizing. By default, the size of the component is driven by the [Size System](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/size-system) and their `size` attribute:

```tsx title="FilmStripDiscreteSizing.tsx"
import { useState } from "react";
import { FilmStripDiscreteControl, OptionView } from "@cutoff/audio-ui-react";

export default function FilmStripDiscreteSizingExample() {
  const [v1, setV1] = useState(4);
  const [v2, setV2] = useState(4);
  const [v3, setV3] = useState(4);
  const [v4, setV4] = useState(4);
  const options = [1, 2, 3, 4, 5, 6, 7, 8];

  return (
    <div className="flex gap-4 items-center flex-wrap">
      <FilmStripDiscreteControl
        value={v1}
        label="Small"
        size="small"
        frameWidth={103}
        frameHeight={260}
        frameCount={8}
        imageHref="/images/demo/traffic-light-filmstrip.png"
        onChange={(e) => setV1(e.value)}
      >
        {options.map((n) => (
          <OptionView key={n} value={n}>State {n}</OptionView>
        ))}
      </FilmStripDiscreteControl>
      <FilmStripDiscreteControl
        value={v2}
        label="Normal"
        size="normal"
        frameWidth={103}
        frameHeight={260}
        frameCount={8}
        imageHref="/images/demo/traffic-light-filmstrip.png"
        onChange={(e) => setV2(e.value)}
      >
        {options.map((n) => (
          <OptionView key={n} value={n}>State {n}</OptionView>
        ))}
      </FilmStripDiscreteControl>
      <FilmStripDiscreteControl
        value={v3}
        label="Large"
        size="large"
        frameWidth={103}
        frameHeight={260}
        frameCount={8}
        imageHref="/images/demo/traffic-light-filmstrip.png"
        onChange={(e) => setV3(e.value)}
      >
        {options.map((n) => (
          <OptionView key={n} value={n}>State {n}</OptionView>
        ))}
      </FilmStripDiscreteControl>
      <FilmStripDiscreteControl
        value={v4}
        label="XLarge"
        size="xlarge"
        frameWidth={103}
        frameHeight={260}
        frameCount={8}
        imageHref="/images/demo/traffic-light-filmstrip.png"
        onChange={(e) => setV4(e.value)}
      >
        {options.map((n) => (
          <OptionView key={n} value={n}>State {n}</OptionView>
        ))}
      </FilmStripDiscreteControl>
    </div>
  );
}
```

Adaptive sizing allows the component to adapt to the size of its parent container, adjusting its dimensions intelligently without causing distortion (`scaleToFit` display mode). The `fill` display mode makes the component fill the entire container, potentially distorting it.

```tsx title="FilmStripDiscreteAdaptiveSize.tsx"
import { useState } from "react";
import { FilmStripDiscreteControl, OptionView } from "@cutoff/audio-ui-react";

export default function FilmStripDiscreteAdaptiveSize() {
  const [wideVal, setWideVal] = useState(4);
  const [tallVal, setTallVal] = useState(4);
  const [distortedVal, setDistortedVal] = useState(4);
  const options = [1, 2, 3, 4, 5, 6, 7, 8];

  return (
    <div className="flex gap-6 items-center flex-wrap">
      <div className="w-28 h-40 border rounded-lg p-2">
        <FilmStripDiscreteControl
          value={wideVal}
          onChange={(e) => setWideVal(e.value)}
          adaptiveSize={true}
          label="Wide"
          frameWidth={103}
          frameHeight={260}
          frameCount={8}
          imageHref="/images/demo/traffic-light-filmstrip.png"
        >
          {options.map((n) => (
            <OptionView key={n} value={n}>State {n}</OptionView>
          ))}
        </FilmStripDiscreteControl>
      </div>
      <div className="w-20 h-48 border rounded-lg p-2">
        <FilmStripDiscreteControl
          value={tallVal}
          onChange={(e) => setTallVal(e.value)}
          adaptiveSize={true}
          label="Tall"
          frameWidth={103}
          frameHeight={260}
          frameCount={8}
          imageHref="/images/demo/traffic-light-filmstrip.png"
        >
          {options.map((n) => (
            <OptionView key={n} value={n}>State {n}</OptionView>
          ))}
        </FilmStripDiscreteControl>
      </div>
      <div className="w-24 h-32 border rounded-lg p-2">
        <FilmStripDiscreteControl
          value={distortedVal}
          onChange={(e) => setDistortedVal(e.value)}
          adaptiveSize={true}
          displayMode="fill"
          label="Distorted"
          frameWidth={103}
          frameHeight={260}
          frameCount={8}
          imageHref="/images/demo/traffic-light-filmstrip.png"
        >
          {options.map((n) => (
            <OptionView key={n} value={n}>State {n}</OptionView>
          ))}
        </FilmStripDiscreteControl>
      </div>
    </div>
  );
}
```

## Common Use Cases

### Traffic Light (Multiple States)

```tsx title="TrafficLight.tsx"
import { useState } from "react";
import { FilmStripDiscreteControl, OptionView } from "@cutoff/audio-ui-react";

export default function TrafficLightExample() {
  const [value, setValue] = useState(4);
  const options = [1, 2, 3, 4, 5, 6, 7, 8];

  return (
    <FilmStripDiscreteControl
      value={value}
      label="Traffic Light"
      size="large"
      frameWidth={103}
      frameHeight={260}
      frameCount={8}
      imageHref="/images/demo/traffic-light-filmstrip.png"
      onChange={(e) => setValue(e.value)}
    >
      {options.map((n) => (
        <OptionView key={n} value={n}>State {n}</OptionView>
      ))}
    </FilmStripDiscreteControl>
  );
}
```

### Waveform Selector

```tsx title="WaveformSelector.tsx"
import { useState } from "react";
import { FilmStripDiscreteControl, OptionView } from "@cutoff/audio-ui-react";

export default function WaveformSelectorExample() {
  const [value, setValue] = useState(4);
  const options = [1, 2, 3, 4, 5, 6, 7, 8];

  return (
    <FilmStripDiscreteControl
      value={value}
      label="Traffic Light"
      size="large"
      frameWidth={103}
      frameHeight={260}
      frameCount={8}
      imageHref="/images/demo/traffic-light-filmstrip.png"
      onChange={(e) => setValue(e.value)}
    >
      {options.map((n) => (
        <OptionView key={n} value={n}>State {n}</OptionView>
      ))}
    </FilmStripDiscreteControl>
  );
}
```

## Filmstrip Image Format

Filmstrip images should follow these conventions:

* **Vertical orientation (default)**: Frames stacked top to bottom
* **Horizontal orientation**: Frames stacked left to right
* **Frame dimensions**: All frames must have identical dimensions
* **Frame count**: Must match the number of discrete options
* **Compatibility**: Works with filmstrips exported from WebKnobMan and other VST development tools

## Next Steps

* Explore [FilmStripContinuousControl](https://cutoff.dev/audio-ui/docs/latest/components/raster/filmstrip-continuous) for continuous parameter control
* Learn about [FilmStripBooleanControl](https://cutoff.dev/audio-ui/docs/latest/components/raster/filmstrip-boolean) for boolean switches
* Check out the [playground](https://playground.cutoff.dev) for interactive examples


---

# FilmStripBooleanControl

# FilmStripBooleanControl

> A boolean control that displays frames from a filmstrip sprite sheet, supporting the industry-standard bitmap-based control representation.

FilmStripBooleanControl is a boolean control that displays frames from a filmstrip sprite sheet. It supports the widely-used industry standard for control representation: bitmap sprite sheets (filmstrips).

While bitmap-based visualization is more constrained than SVG (no dynamic theming, fixed visual appearance), this component shares the same layout, parameter model, interaction, and accessibility as other controls (vector, raster, and control primitives); see the [Raster introduction](https://cutoff.dev/audio-ui/docs/latest/components/raster/introduction).

Typically uses 2 frames: frame 0 for false/off, frame 1 for true/on.

> **Note:** **No Theming Support**: This component does NOT support themable props (color, roundness, thickness) as visuals are entirely determined by the image content.

## Props

| Name          | Type                                                   | Default  | Required | Description                                                                                                                                         |
| :------------ | :----------------------------------------------------- | :------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------- |
| value         | boolean                                                | -        | Yes      | Current value of the control                                                                                                                        |
| onChange      | (event: AudioControlEvent\<boolean>) => void           | -        | No       | Handler for value changes                                                                                                                           |
| latch         | boolean                                                | false    | No       | Whether the button operates in latch (toggle) mode or momentary mode                                                                                |
| label         | string                                                 | -        | No       | Label displayed below the component                                                                                                                 |
| frameWidth    | number                                                 | -        | Yes      | Width of a single frame in the filmstrip (pixels)                                                                                                   |
| frameHeight   | number                                                 | -        | Yes      | Height of a single frame in the filmstrip (pixels)                                                                                                  |
| frameCount    | number                                                 | -        | Yes      | Total number of frames in the filmstrip sprite sheet (typically 2 for boolean controls)                                                             |
| imageHref     | string                                                 | -        | Yes      | URL to the sprite sheet/filmstrip image                                                                                                             |
| orientation   | "vertical" \| "horizontal"                             | vertical | No       | Orientation of the filmstrip (frames stacked vertically or horizontally)                                                                            |
| frameRotation | number                                                 | 0        | No       | Optional frame rotation in degrees                                                                                                                  |
| invertValue   | boolean                                                | false    | No       | If true, inverts the normalized value (0.0 -> 1.0 and 1.0 -> 0.0). Useful for filmstrips where frame 0 represents "on" and frame 1 represents "off" |
| adaptiveSize  | boolean                                                | false    | No       | Whether the component stretches to fill its container                                                                                               |
| size          | "xsmall" \| "small" \| "normal" \| "large" \| "xlarge" | normal   | No       | Size of the component                                                                                                                               |
| parameter     | BooleanParameter                                       | -        | No       | Audio Parameter definition (Model). If provided, overrides label/latch from ad-hoc props                                                            |

## Basic Usage

FilmStripBooleanControl requires a filmstrip sprite sheet image with typically 2 frames (off and on states):

```tsx title="BasicFilmStripBoolean.tsx"
import { useState } from "react";
import { FilmStripBooleanControl } from "@cutoff/audio-ui-react";

export default function BasicFilmStripBooleanExample() {
  const [isOn, setIsOn] = useState(false);

  return (
    <FilmStripBooleanControl
      value={isOn}
      latch={true}
      label="Power"
      size="large"
      frameWidth={64}
      frameHeight={64}
      frameCount={2}
      imageHref="/images/demo/switch_metal.png"
      onChange={(e) => setIsOn(e.value)}
    />
  );
}
```

## Modes of Operation

### Latch Mode (Toggle)

In latch mode, the button toggles between pressed and released states:

```tsx title="LatchFilmStripBoolean.tsx"
import { useState } from "react";
import { FilmStripBooleanControl } from "@cutoff/audio-ui-react";

export default function LatchFilmStripBooleanExample() {
  const [isOn, setIsOn] = useState(false);

  return (
    <FilmStripBooleanControl
      value={isOn}
      latch={true}
      label="Power"
      size="large"
      frameWidth={64}
      frameHeight={64}
      frameCount={2}
      imageHref="/images/demo/switch_metal.png"
      onChange={(e) => setIsOn(e.value)}
    />
  );
}
```

### Momentary Mode

In momentary mode, the button is only active while being pressed:

```tsx title="MomentaryFilmStripBoolean.tsx"
import { useState } from "react";
import { FilmStripBooleanControl } from "@cutoff/audio-ui-react";

export default function MomentaryFilmStripBooleanExample() {
  const [isPressed, setIsPressed] = useState(false);

  return (
    <FilmStripBooleanControl
      value={isPressed}
      latch={false}
      label="Record"
      size="large"
      frameWidth={128}
      frameHeight={128}
      frameCount={2}
      imageHref="/images/demo/big_button.png"
      onChange={(e) => setIsPressed(e.value)}
    />
  );
}
```

## Filmstrip Configuration

### Frame Dimensions

Specify the dimensions of a single frame:

```tsx title="FrameDimensions.tsx"
import { useState } from "react";
import { FilmStripBooleanControl } from "@cutoff/audio-ui-react";

export default function FrameDimensionsExample() {
  const [isOn, setIsOn] = useState(false);

  return (
    <FilmStripBooleanControl
      value={isOn}
      latch={true}
      label="Power Switch"
      size="large"
      frameWidth={64}
      frameHeight={64}
      frameCount={2}
      imageHref="/images/demo/switch_metal.png"
      onChange={(e) => setIsOn(e.value)}
    />
  );
}
```

### Orientation

Control whether frames are stacked vertically or horizontally:

```tsx title="Orientation.tsx"
import { useState } from "react";
import { FilmStripBooleanControl } from "@cutoff/audio-ui-react";

export default function OrientationExample() {
  const [isOn, setIsOn] = useState(false);

  return (
    <FilmStripBooleanControl
      value={isOn}
      latch={true}
      label="Switch"
      size="large"
      frameWidth={64}
      frameHeight={64}
      frameCount={2}
      imageHref="/images/demo/switch_metal.png"
      orientation="vertical"
      onChange={(e) => setIsOn(e.value)}
    />
  );
}
```

### Frame Rotation

Rotate the entire filmstrip container:

```tsx title="FrameRotation.tsx"
import { useState } from "react";
import { FilmStripBooleanControl } from "@cutoff/audio-ui-react";

export default function FrameRotationExample() {
  const [isOn, setIsOn] = useState(false);

  return (
    <FilmStripBooleanControl
      value={isOn}
      latch={true}
      label="Rotated Switch"
      size="large"
      frameWidth={64}
      frameHeight={64}
      frameCount={2}
      imageHref="/images/demo/switch_metal.png"
      frameRotation={90}
      onChange={(e) => setIsOn(e.value)}
    />
  );
}
```

### Value Inversion

Invert the value mapping for filmstrips where frame 0 represents "on" and frame 1 represents "off":

```tsx title="ValueInversion.tsx"
import { useState } from "react";
import { FilmStripBooleanControl } from "@cutoff/audio-ui-react";

export default function ValueInversionExample() {
  const [isOn, setIsOn] = useState(false);

  return (
    <FilmStripBooleanControl
      value={isOn}
      latch={true}
      label="Power"
      size="large"
      frameWidth={64}
      frameHeight={64}
      frameCount={2}
      imageHref="/images/demo/switch_metal.png"
      invertValue={true}
      onChange={(e) => setIsOn(e.value)}
    />
  );
}
```

## Frame Mapping

The component maps boolean values to frames:

* `false` (normalized 0.0) → Frame 0
* `true` (normalized 1.0) → Frame 1

When `invertValue` is true, the mapping is reversed:

* `false` → Frame 1
* `true` → Frame 0

## Parameter Model

Use a parameter model for integration with audio parameter systems. The following example uses ad-hoc props; with the full library you would use `createBooleanParameter` and the `parameter` prop:

```tsx title="FilmStripBooleanParameterModel.tsx"
import { useState } from "react";
import { FilmStripBooleanControl } from "@cutoff/audio-ui-react";

export default function FilmStripBooleanParameterModelExample() {
  const [value, setValue] = useState(false);

  return (
    <FilmStripBooleanControl
      label="Power"
      latch={true}
      value={value}
      size="large"
      frameWidth={64}
      frameHeight={64}
      frameCount={2}
      imageHref="/images/demo/switch_metal.png"
      onChange={(e) => setValue(e.value)}
    />
  );
}
```

## Adaptive Sizing

The FilmStripBooleanControl component supports both fixed sizes and adaptive sizing. By default, the size of the component is driven by the [Size System](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/size-system) and their `size` attribute:

```tsx title="FilmStripBooleanSizing.tsx"
import { useState } from "react";
import { FilmStripBooleanControl } from "@cutoff/audio-ui-react";

export default function FilmStripBooleanSizingExample() {
  const [v1, setV1] = useState(false);
  const [v2, setV2] = useState(false);
  const [v3, setV3] = useState(false);
  const [v4, setV4] = useState(false);

  return (
    <div className="flex gap-4 items-center flex-wrap">
      <FilmStripBooleanControl
        value={v1}
        latch={true}
        label="Small"
        size="small"
        frameWidth={64}
        frameHeight={64}
        frameCount={2}
        imageHref="/images/demo/switch_metal.png"
        onChange={(e) => setV1(e.value)}
      />
      <FilmStripBooleanControl
        value={v2}
        latch={true}
        label="Normal"
        size="normal"
        frameWidth={64}
        frameHeight={64}
        frameCount={2}
        imageHref="/images/demo/switch_metal.png"
        onChange={(e) => setV2(e.value)}
      />
      <FilmStripBooleanControl
        value={v3}
        latch={true}
        label="Large"
        size="large"
        frameWidth={64}
        frameHeight={64}
        frameCount={2}
        imageHref="/images/demo/switch_metal.png"
        onChange={(e) => setV3(e.value)}
      />
      <FilmStripBooleanControl
        value={v4}
        latch={true}
        label="XLarge"
        size="xlarge"
        frameWidth={64}
        frameHeight={64}
        frameCount={2}
        imageHref="/images/demo/switch_metal.png"
        onChange={(e) => setV4(e.value)}
      />
    </div>
  );
}
```

Adaptive sizing allows the component to adapt to the size of its parent container, adjusting its dimensions intelligently without causing distortion (`scaleToFit` display mode). The `fill` display mode makes the component fill the entire container, potentially distorting it.

```tsx title="FilmStripBooleanAdaptiveSize.tsx"
import { useState } from "react";
import { FilmStripBooleanControl } from "@cutoff/audio-ui-react";

export default function FilmStripBooleanAdaptiveSize() {
  const [wideVal, setWideVal] = useState(false);
  const [tallVal, setTallVal] = useState(false);
  const [distortedVal, setDistortedVal] = useState(false);

  return (
    <div className="flex gap-6 items-center flex-wrap">
      <div className="w-32 h-12 border rounded-lg p-2">
        <FilmStripBooleanControl
          value={wideVal}
          onChange={(e) => setWideVal(e.value)}
          latch={true}
          adaptiveSize={true}
          label="Wide"
          frameWidth={64}
          frameHeight={64}
          frameCount={2}
          imageHref="/images/demo/switch_metal.png"
        />
      </div>
      <div className="w-12 h-32 border rounded-lg p-2">
        <FilmStripBooleanControl
          value={tallVal}
          onChange={(e) => setTallVal(e.value)}
          latch={true}
          adaptiveSize={true}
          label="Tall"
          frameWidth={64}
          frameHeight={64}
          frameCount={2}
          imageHref="/images/demo/switch_metal.png"
        />
      </div>
      <div className="w-24 h-20 border rounded-lg p-2">
        <FilmStripBooleanControl
          value={distortedVal}
          onChange={(e) => setDistortedVal(e.value)}
          latch={true}
          adaptiveSize={true}
          displayMode="fill"
          label="Distorted"
          frameWidth={64}
          frameHeight={64}
          frameCount={2}
          imageHref="/images/demo/switch_metal.png"
        />
      </div>
    </div>
  );
}
```

## Common Use Cases

### Power Switch

```tsx title="PowerSwitch.tsx"
import { useState } from "react";
import { FilmStripBooleanControl } from "@cutoff/audio-ui-react";

export default function PowerSwitchExample() {
  const [isOn, setIsOn] = useState(false);

  return (
    <FilmStripBooleanControl
      value={isOn}
      latch={true}
      label="Power"
      size="large"
      frameWidth={64}
      frameHeight={64}
      frameCount={2}
      imageHref="/images/demo/switch_metal.png"
      invertValue={true}
      onChange={(e) => setIsOn(e.value)}
    />
  );
}
```

### Momentary Button

```tsx title="MomentaryButton.tsx"
import { useState } from "react";
import { FilmStripBooleanControl } from "@cutoff/audio-ui-react";

export default function MomentaryButtonExample() {
  const [isPressed, setIsPressed] = useState(false);

  return (
    <FilmStripBooleanControl
      value={isPressed}
      latch={false}
      label="Record"
      size="large"
      frameWidth={64}
      frameHeight={64}
      frameCount={2}
      imageHref="/images/demo/big_button.png"
      onChange={(e) => setIsPressed(e.value)}
    />
  );
}
```

### Metal Switch

```tsx title="MetalSwitch.tsx"
import { useState } from "react";
import { FilmStripBooleanControl } from "@cutoff/audio-ui-react";

export default function MetalSwitchExample() {
  const [isOn, setIsOn] = useState(false);

  return (
    <FilmStripBooleanControl
      value={isOn}
      latch={true}
      label="Metal Switch"
      size="large"
      frameWidth={64}
      frameHeight={64}
      frameCount={2}
      imageHref="/images/demo/switch_metal.png"
      invertValue={true}
      onChange={(e) => setIsOn(e.value)}
    />
  );
}
```

## Filmstrip Image Format

Filmstrip images should follow these conventions:

* **Frame count**: Typically 2 frames for boolean controls (off and on)
* **Vertical orientation (default)**: Frames stacked top to bottom
* **Horizontal orientation**: Frames stacked left to right
* **Frame dimensions**: All frames must have identical dimensions
* **Compatibility**: Works with filmstrips exported from WebKnobMan and other VST development tools

## Next Steps

* Explore [FilmStripContinuousControl](https://cutoff.dev/audio-ui/docs/latest/components/raster/filmstrip-continuous) for continuous parameter control
* Learn about [FilmStripDiscreteControl](https://cutoff.dev/audio-ui/docs/latest/components/raster/filmstrip-discrete) for discrete parameter control
* Check out the [playground](https://playground.cutoff.dev) for interactive examples


---

# ImageKnob

# ImageKnob

> A continuous control that rotates an image based on a normalized value, perfect for custom knob designs using bitmap graphics.

ImageKnob is a continuous control that rotates an image based on a normalized value. It shares the same layout, parameter model, interaction, and accessibility as other controls (vector, raster, and control primitives); see the [Raster introduction](https://cutoff.dev/audio-ui/docs/latest/components/raster/introduction).

The image rotation is determined by the normalized value (0-1 maps to rotation based on openness and rotation offset).

> **Note:** **No Theming Support**: This component does NOT support themable props (color, roundness, thickness) as visuals are entirely determined by the image content.

## Props

| Name                   | Type                                                                 | Default   | Required | Description                                                                                             |
| :--------------------- | :------------------------------------------------------------------- | :-------- | :------- | :------------------------------------------------------------------------------------------------------ |
| value                  | number                                                               | -         | Yes      | Current value of the control                                                                            |
| onChange               | (event: AudioControlEvent\<number>) => void                          | -         | No       | Handler for value changes                                                                               |
| min                    | number                                                               | -         | No       | Minimum value (ad-hoc mode)                                                                             |
| max                    | number                                                               | -         | No       | Maximum value (ad-hoc mode)                                                                             |
| step                   | number                                                               | -         | No       | Step size for value adjustments                                                                         |
| bipolar                | boolean                                                              | false     | No       | Whether the component should operate in bipolar mode (centered at zero)                                 |
| label                  | string                                                               | -         | No       | Label displayed below the component                                                                     |
| frameWidth             | number                                                               | -         | Yes      | Width of the viewBox (determines viewBox width)                                                         |
| frameHeight            | number                                                               | -         | Yes      | Height of the viewBox (determines viewBox height)                                                       |
| imageHref              | string                                                               | -         | Yes      | URL to the image to rotate                                                                              |
| rotation               | number                                                               | 0         | No       | Optional rotation angle offset in degrees                                                               |
| openness               | number                                                               | 90        | No       | Openness of the arc in degrees (value between 0-360º; 0º closed, 90º 3/4 open, 180º half-circle)        |
| valueAsLabel           | "labelOnly" \| "valueOnly" \| "interactive"                          | labelOnly | No       | Controls how the label and value are displayed                                                          |
| adaptiveSize           | boolean                                                              | false     | No       | Whether the component stretches to fill its container                                                   |
| size                   | "xsmall" \| "small" \| "normal" \| "large" \| "xlarge"               | normal    | No       | Size of the component                                                                                   |
| parameter              | ContinuousParameter                                                  | -         | No       | Audio Parameter definition (Model). If provided, overrides min/max/step/label/bipolar from ad-hoc props |
| valueFormatter         | (value: number, parameterDef: AudioParameter) => string \| undefined | -         | No       | Custom renderer for the value display                                                                   |
| interactionMode        | "drag" \| "wheel" \| "both"                                          | both      | No       | Interaction mode                                                                                        |
| interactionDirection   | "vertical" \| "horizontal" \| "circular" \| "both"                   | -         | No       | Direction of interaction                                                                                |
| interactionSensitivity | number                                                               | -         | No       | Sensitivity multiplier for interactions (default varies by control type)                                |

## Basic Usage

ImageKnob requires an image URL and frame dimensions:

```tsx title="BasicImageKnob.tsx"
import { useState } from "react";
import { ImageKnob } from "@cutoff/audio-ui-react";

export default function BasicImageKnobExample() {
  const [value, setValue] = useState(50);

  return (
    <ImageKnob
      value={value}
      min={0}
      max={100}
      label="Volume"
      size="large"
      frameWidth={100}
      frameHeight={100}
      imageHref="/images/demo/knob-volume.png"
      onChange={(e) => setValue(e.value)}
    />
  );
}
```

## Rotation Configuration

### Openness

Control the rotation range using the `openness` prop:

```tsx title="Openness.tsx"
import { useState } from "react";
import { ImageKnob } from "@cutoff/audio-ui-react";

export default function OpennessExample() {
  const [v90, setV90] = useState(50);
  const [v180, setV180] = useState(50);
  const [v270, setV270] = useState(50);

  return (
    <div className="flex gap-4 items-center flex-wrap">
      <ImageKnob
        value={v90}
        min={0}
        max={100}
        label="90° Open"
        size="large"
        frameWidth={100}
        frameHeight={100}
        imageHref="/images/demo/knob-volume.png"
        openness={90}
        onChange={(e) => setV90(e.value)}
      />
      <ImageKnob
        value={v180}
        min={0}
        max={100}
        label="180° Open"
        size="large"
        frameWidth={100}
        frameHeight={100}
        imageHref="/images/demo/knob-volume.png"
        openness={180}
        onChange={(e) => setV180(e.value)}
      />
      <ImageKnob
        value={v270}
        min={0}
        max={100}
        label="270° Open"
        size="large"
        frameWidth={100}
        frameHeight={100}
        imageHref="/images/demo/knob-volume.png"
        openness={270}
        onChange={(e) => setV270(e.value)}
      />
    </div>
  );
}
```

### Rotation Offset

Apply a rotation offset to align the image:

```tsx title="RotationOffset.tsx"
import { useState } from "react";
import { ImageKnob } from "@cutoff/audio-ui-react";

export default function RotationOffsetExample() {
  const [zeroVal, setZeroVal] = useState(50);
  const [rot45Val, setRot45Val] = useState(50);

  return (
    <div className="flex gap-4 items-center flex-wrap">
      <ImageKnob
        value={zeroVal}
        min={0}
        max={100}
        label="No Offset"
        size="large"
        frameWidth={100}
        frameHeight={100}
        imageHref="/images/demo/knob-volume.png"
        rotation={0}
        onChange={(e) => setZeroVal(e.value)}
      />
      <ImageKnob
        value={rot45Val}
        min={0}
        max={100}
        label="45° Offset"
        size="large"
        frameWidth={100}
        frameHeight={100}
        imageHref="/images/demo/knob-volume.png"
        rotation={45}
        onChange={(e) => setRot45Val(e.value)}
      />
    </div>
  );
}
```

## Parameter Model

Use a parameter model for integration with audio parameter systems. The following example uses ad-hoc props; with the full library you would use `createContinuousParameter` and the `parameter` prop:

```tsx title="ImageKnobParameterModel.tsx"
import { useState } from "react";
import { ImageKnob } from "@cutoff/audio-ui-react";

export default function ImageKnobParameterModelExample() {
  const [value, setValue] = useState(50);

  return (
    <ImageKnob
      min={0}
      max={100}
      label="Volume"
      value={value}
      size="large"
      frameWidth={100}
      frameHeight={100}
      imageHref="/images/demo/knob-volume.png"
      onChange={(e) => setValue(e.value)}
    />
  );
}
```

## Adaptive Sizing

The ImageKnob component supports both fixed sizes and adaptive sizing. By default, the size of the component is driven by the [Size System](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/size-system) and their `size` attribute:

```tsx title="ImageKnobSizing.tsx"
import { useState } from "react";
import { ImageKnob } from "@cutoff/audio-ui-react";

export default function ImageKnobSizingExample() {
  const [v1, setV1] = useState(50);
  const [v2, setV2] = useState(50);
  const [v3, setV3] = useState(50);
  const [v4, setV4] = useState(50);

  return (
    <div className="flex gap-4 items-center flex-wrap">
      <ImageKnob
        value={v1}
        min={0}
        max={100}
        label="Small"
        size="small"
        frameWidth={100}
        frameHeight={100}
        imageHref="/images/demo/knob-volume.png"
        onChange={(e) => setV1(e.value)}
      />
      <ImageKnob
        value={v2}
        min={0}
        max={100}
        label="Normal"
        size="normal"
        frameWidth={100}
        frameHeight={100}
        imageHref="/images/demo/knob-volume.png"
        onChange={(e) => setV2(e.value)}
      />
      <ImageKnob
        value={v3}
        min={0}
        max={100}
        label="Large"
        size="large"
        frameWidth={100}
        frameHeight={100}
        imageHref="/images/demo/knob-volume.png"
        onChange={(e) => setV3(e.value)}
      />
      <ImageKnob
        value={v4}
        min={0}
        max={100}
        label="XLarge"
        size="xlarge"
        frameWidth={100}
        frameHeight={100}
        imageHref="/images/demo/knob-volume.png"
        onChange={(e) => setV4(e.value)}
      />
    </div>
  );
}
```

Adaptive sizing allows the component to adapt to the size of its parent container, adjusting its dimensions intelligently without causing distortion (`scaleToFit` display mode). The `fill` display mode makes the component fill the entire container, potentially distorting it.

```tsx title="ImageKnobAdaptiveSize.tsx"
import { useState } from "react";
import { ImageKnob } from "@cutoff/audio-ui-react";

export default function ImageKnobAdaptiveSize() {
  const [wideVal, setWideVal] = useState(50);
  const [tallVal, setTallVal] = useState(50);
  const [distortedVal, setDistortedVal] = useState(50);

  return (
    <div className="flex gap-6 items-center flex-wrap">
      <div className="w-48 h-16 border rounded-lg p-2">
        <ImageKnob
          value={wideVal}
          min={0}
          max={100}
          onChange={(e) => setWideVal(e.value)}
          adaptiveSize={true}
          label="Wide"
          frameWidth={100}
          frameHeight={100}
          imageHref="/images/demo/knob-volume.png"
        />
      </div>
      <div className="w-24 h-48 border rounded-lg p-2">
        <ImageKnob
          value={tallVal}
          min={0}
          max={100}
          onChange={(e) => setTallVal(e.value)}
          adaptiveSize={true}
          label="Tall"
          frameWidth={100}
          frameHeight={100}
          imageHref="/images/demo/knob-volume.png"
        />
      </div>
      <div className="w-32 h-20 border rounded-lg p-2">
        <ImageKnob
          value={distortedVal}
          min={0}
          max={100}
          onChange={(e) => setDistortedVal(e.value)}
          adaptiveSize={true}
          displayMode="fill"
          label="Distorted"
          frameWidth={100}
          frameHeight={100}
          imageHref="/images/demo/knob-volume.png"
        />
      </div>
    </div>
  );
}
```

## Interaction Modes

Control how users interact with the knob:

```tsx title="ImageKnobInteraction.tsx"
import { useState } from "react";
import { ImageKnob } from "@cutoff/audio-ui-react";

export default function ImageKnobInteractionExample() {
  const [dragVal, setDragVal] = useState(50);
  const [wheelVal, setWheelVal] = useState(50);

  return (
    <div className="flex gap-4 items-center flex-wrap">
      <ImageKnob
        value={dragVal}
        min={0}
        max={100}
        label="Drag Only"
        interactionMode="drag"
        size="large"
        frameWidth={100}
        frameHeight={100}
        imageHref="/images/demo/knob-volume.png"
        onChange={(e) => setDragVal(e.value)}
      />
      <ImageKnob
        value={wheelVal}
        min={0}
        max={100}
        label="Wheel Only"
        interactionMode="wheel"
        size="large"
        frameWidth={100}
        frameHeight={100}
        imageHref="/images/demo/knob-volume.png"
        onChange={(e) => setWheelVal(e.value)}
      />
    </div>
  );
}
```

## Common Use Cases

### Volume Knob

```tsx title="VolumeKnob.tsx"
import { useState } from "react";
import { ImageKnob } from "@cutoff/audio-ui-react";

export default function VolumeKnobExample() {
  const [value, setValue] = useState(50);

  return (
    <ImageKnob
      value={value}
      min={0}
      max={100}
      label="Volume"
      size="large"
      frameWidth={100}
      frameHeight={100}
      imageHref="/images/demo/knob-volume.png"
      valueAsLabel="interactive"
      onChange={(e) => setValue(e.value)}
    />
  );
}
```

### Tone Knob

```tsx title="ToneKnob.tsx"
import { useState } from "react";
import { ImageKnob } from "@cutoff/audio-ui-react";

export default function ToneKnobExample() {
  const [value, setValue] = useState(5);

  return (
    <ImageKnob
      value={value}
      min={1}
      max={10}
      label="Tone"
      size="large"
      frameWidth={100}
      frameHeight={100}
      imageHref="/images/demo/knob-tone.png"
      valueAsLabel="interactive"
      onChange={(e) => setValue(e.value)}
    />
  );
}
```

### Direction Indicator

```tsx title="DirectionIndicator.tsx"
import { useState } from "react";
import { ImageKnob } from "@cutoff/audio-ui-react";

export default function DirectionIndicatorExample() {
  const [value, setValue] = useState(3);

  return (
    <ImageKnob
      value={value}
      min={0}
      max={10}
      label="Direction"
      size="large"
      frameWidth={100}
      frameHeight={100}
      imageHref="/images/demo/finger-pointing.png"
      onChange={(e) => setValue(e.value)}
    />
  );
}
```

## Image Requirements

* **Format**: Any image format supported by browsers (PNG, JPG, SVG, etc.)
* **Dimensions**: The `frameWidth` and `frameHeight` props determine the viewBox dimensions
* **Aspect Ratio**: The image will be scaled to fit the viewBox while preserving aspect ratio
* **Rotation**: The image rotates around its center point based on the normalized value

## Rotation Calculation

The rotation angle is calculated as:

* Normalized value (0-1) maps to rotation range based on `openness`
* `rotation` offset is added to align the image
* For bipolar mode, the rotation is centered around the midpoint

## Next Steps

* Explore [ImageRotarySwitch](https://cutoff.dev/audio-ui/docs/latest/components/raster/image-rotary-switch) for discrete rotary controls
* Learn about [ImageSwitch](https://cutoff.dev/audio-ui/docs/latest/components/raster/image-switch) for boolean switches
* Check out the [playground](https://playground.cutoff.dev) for interactive examples


---

# ImageRotarySwitch

# ImageRotarySwitch

> A discrete control that rotates an image based on discrete option values, perfect for custom rotary switch designs using bitmap graphics.

ImageRotarySwitch is a discrete control that rotates an image based on discrete option values. It shares the same layout, parameter model, interaction, and accessibility as other controls (vector, raster, and control primitives); see the [Raster introduction](https://cutoff.dev/audio-ui/docs/latest/components/raster/introduction).

The image rotation is determined by mapping the current value to a normalized position, with snapping to discrete positions based on the number of options.

> **Note:** **No Theming Support**: This component does NOT support themable props (color, roundness, thickness) as visuals are entirely determined by the image content.

## Props

| Name         | Type                                                                   | Default | Required | Description                                                                                      |
| :----------- | :--------------------------------------------------------------------- | :------ | :------- | :----------------------------------------------------------------------------------------------- |
| value        | string \| number                                                       | -       | Yes      | Current value of the control                                                                     |
| onChange     | (event: AudioControlEvent\<string \| number>) => void                  | -       | No       | Handler for value changes                                                                        |
| label        | string                                                                 | -       | No       | Label displayed below the component                                                              |
| frameWidth   | number                                                                 | -       | Yes      | Width of the viewBox (determines viewBox width)                                                  |
| frameHeight  | number                                                                 | -       | Yes      | Height of the viewBox (determines viewBox height)                                                |
| imageHref    | string                                                                 | -       | Yes      | URL to the image to rotate                                                                       |
| rotation     | number                                                                 | 0       | No       | Optional rotation angle offset in degrees                                                        |
| openness     | number                                                                 | 90      | No       | Openness of the arc in degrees (value between 0-360º; 0º closed, 90º 3/4 open, 180º half-circle) |
| adaptiveSize | boolean                                                                | false   | No       | Whether the component stretches to fill its container                                            |
| size         | "xsmall" \| "small" \| "normal" \| "large" \| "xlarge"                 | normal  | No       | Size of the component                                                                            |
| parameter    | DiscreteParameter                                                      | -       | No       | Audio Parameter definition (Model). If provided, overrides options/label from ad-hoc props       |
| options      | Array<{ value: string \| number; label?: string; midiValue?: number }> | -       | No       | Array of option definitions (strict mode)                                                        |
| children     | React.ReactNode                                                        | -       | No       | OptionView children for ad-hoc or hybrid mode                                                    |

## Understanding Options vs Children

ImageRotarySwitch distinguishes between two concepts:

* **`options` prop**: Defines the parameter model (value, label, midiValue). Used for parameter structure.
* **`children` (OptionView components)**: In this component, discrete visuals are handled by the raster image (rotation), not by OptionView content. OptionView here is mainly syntactic sugar: it populates the option set in ad-hoc mode (values and count); the component uses the number of OptionView children to derive rotation position. The visual content inside OptionView (e.g. text or icons) is never displayed.

These serve different purposes and can be used together: use `options` or a `parameter` when you have data-driven option definitions or need MIDI mapping; use OptionView children for simple, ad-hoc setups when you want the option set inferred from the children.

> **Note:** **Basic use**: For basic use of ImageRotarySwitch, and especially when using ad-hoc props (no `parameter` prop), prefer **OptionView** children. In that case the option set is inferred from the OptionViews. Use the `options` prop or a full parameter when you need explicit parameter structure (e.g. data-driven options, MIDI mapping). For a full disambiguation of options vs OptionView, see [Options vs OptionView](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/options-and-optionview).

## Basic Usage

ImageRotarySwitch requires an image URL and frame dimensions. Use OptionView children to define the discrete options:

```tsx title="BasicImageRotarySwitch.tsx"
import { useState } from "react";
import { ImageRotarySwitch, OptionView } from "@cutoff/audio-ui-react";

export default function BasicImageRotarySwitchExample() {
  const [value, setValue] = useState("low");

  return (
    <ImageRotarySwitch
      value={value}
      label="Level"
      size="large"
      frameWidth={100}
      frameHeight={100}
      imageHref="/images/demo/knob-volume.png"
      onChange={(e) => setValue(e.value)}
    >
      <OptionView value="low">Low</OptionView>
      <OptionView value="medium">Medium</OptionView>
      <OptionView value="high">High</OptionView>
    </ImageRotarySwitch>
  );
}
```

## Modes of Operation

ImageRotarySwitch supports three modes of operation:

### Ad-Hoc Mode (Children Only)

Model inferred from OptionView children:

```tsx title="AdHocImageRotarySwitch.tsx"
import { useState } from "react";
import { ImageRotarySwitch, OptionView } from "@cutoff/audio-ui-react";

export default function AdHocImageRotarySwitchExample() {
  const [value, setValue] = useState("low");

  return (
    <ImageRotarySwitch
      value={value}
      label="Level"
      size="large"
      frameWidth={100}
      frameHeight={100}
      imageHref="/images/demo/knob-volume.png"
      onChange={(e) => setValue(e.value)}
    >
      <OptionView value="low">Low</OptionView>
      <OptionView value="medium">Medium</OptionView>
      <OptionView value="high">High</OptionView>
    </ImageRotarySwitch>
  );
}
```

### Strict Mode (Parameter Only)

Model provided via parameter prop. The following example uses children (OptionView); with the full library you would use `createDiscreteParameter` and the `parameter` prop:

```tsx title="StrictModeImageRotarySwitch.tsx"
import { useState } from "react";
import { ImageRotarySwitch, OptionView } from "@cutoff/audio-ui-react";

export default function StrictModeImageRotarySwitchExample() {
  const [value, setValue] = useState("low");

  return (
    <ImageRotarySwitch
      value={value}
      label="Level"
      size="large"
      frameWidth={100}
      frameHeight={100}
      imageHref="/images/demo/knob-volume.png"
      onChange={(e) => setValue(e.value)}
    >
      <OptionView value="low">Low</OptionView>
      <OptionView value="medium">Medium</OptionView>
      <OptionView value="high">High</OptionView>
    </ImageRotarySwitch>
  );
}
```

### Hybrid Mode (Parameter + Children)

Model from parameter, View from children (matched by value). Use the same pattern as Strict Mode with OptionView children when the parameter is not in scope.

```tsx title="HybridModeImageRotarySwitch.tsx"
import { useState } from "react";
import { ImageRotarySwitch, OptionView } from "@cutoff/audio-ui-react";

export default function HybridModeImageRotarySwitchExample() {
  const [value, setValue] = useState("low");

  return (
    <ImageRotarySwitch
      value={value}
      label="Level"
      size="large"
      frameWidth={100}
      frameHeight={100}
      imageHref="/images/demo/knob-volume.png"
      onChange={(e) => setValue(e.value)}
    >
      <OptionView value="low">Low</OptionView>
      <OptionView value="medium">Medium</OptionView>
      <OptionView value="high">High</OptionView>
    </ImageRotarySwitch>
  );
}
```

## Rotation Configuration

### Openness

Control the rotation range using the `openness` prop:

```tsx title="Openness.tsx"
import { useState } from "react";
import { ImageRotarySwitch, OptionView } from "@cutoff/audio-ui-react";

export default function OpennessExample() {
  const [value, setValue] = useState("medium");

  return (
    <ImageRotarySwitch
      value={value}
      label="90° Open"
      size="large"
      frameWidth={100}
      frameHeight={100}
      imageHref="/images/demo/knob-volume.png"
      openness={90}
      onChange={(e) => setValue(e.value)}
    >
      <OptionView value="low">Low</OptionView>
      <OptionView value="medium">Medium</OptionView>
      <OptionView value="high">High</OptionView>
    </ImageRotarySwitch>
  );
}
```

### Rotation Offset

Apply a rotation offset to align the image:

```tsx title="RotationOffset.tsx"
import { useState } from "react";
import { ImageRotarySwitch, OptionView } from "@cutoff/audio-ui-react";

export default function RotationOffsetExample() {
  const [value, setValue] = useState("medium");

  return (
    <ImageRotarySwitch
      value={value}
      label="No Offset"
      size="large"
      frameWidth={100}
      frameHeight={100}
      imageHref="/images/demo/knob-volume.png"
      rotation={0}
      onChange={(e) => setValue(e.value)}
    >
      <OptionView value="low">Low</OptionView>
      <OptionView value="medium">Medium</OptionView>
      <OptionView value="high">High</OptionView>
    </ImageRotarySwitch>
  );
}
```

## Position Mapping

The component maps discrete option values to rotation positions:

* Option index 0 → Minimum rotation position
* Option index N → Maximum rotation position
* Intermediate positions are evenly distributed across the openness range

## Adaptive Sizing

The ImageRotarySwitch component supports both fixed sizes and adaptive sizing. By default, the size of the component is driven by the [Size System](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/size-system) and their `size` attribute:

```tsx title="ImageRotarySwitchSizing.tsx"
import { useState } from "react";
import { ImageRotarySwitch, OptionView } from "@cutoff/audio-ui-react";

export default function ImageRotarySwitchSizingExample() {
  const [v1, setV1] = useState("medium");
  const [v2, setV2] = useState("medium");
  const [v3, setV3] = useState("medium");
  const [v4, setV4] = useState("medium");

  return (
    <div className="flex gap-4 items-center flex-wrap">
      <ImageRotarySwitch
        value={v1}
        label="Small"
        size="small"
        frameWidth={100}
        frameHeight={100}
        imageHref="/images/demo/knob-volume.png"
        onChange={(e) => setV1(e.value)}
      >
        <OptionView value="low">Low</OptionView>
        <OptionView value="medium">Medium</OptionView>
        <OptionView value="high">High</OptionView>
      </ImageRotarySwitch>
      <ImageRotarySwitch
        value={v2}
        label="Normal"
        size="normal"
        frameWidth={100}
        frameHeight={100}
        imageHref="/images/demo/knob-volume.png"
        onChange={(e) => setV2(e.value)}
      >
        <OptionView value="low">Low</OptionView>
        <OptionView value="medium">Medium</OptionView>
        <OptionView value="high">High</OptionView>
      </ImageRotarySwitch>
      <ImageRotarySwitch
        value={v3}
        label="Large"
        size="large"
        frameWidth={100}
        frameHeight={100}
        imageHref="/images/demo/knob-volume.png"
        onChange={(e) => setV3(e.value)}
      >
        <OptionView value="low">Low</OptionView>
        <OptionView value="medium">Medium</OptionView>
        <OptionView value="high">High</OptionView>
      </ImageRotarySwitch>
      <ImageRotarySwitch
        value={v4}
        label="XLarge"
        size="xlarge"
        frameWidth={100}
        frameHeight={100}
        imageHref="/images/demo/knob-volume.png"
        onChange={(e) => setV4(e.value)}
      >
        <OptionView value="low">Low</OptionView>
        <OptionView value="medium">Medium</OptionView>
        <OptionView value="high">High</OptionView>
      </ImageRotarySwitch>
    </div>
  );
}
```

Adaptive sizing allows the component to adapt to the size of its parent container, adjusting its dimensions intelligently without causing distortion (`scaleToFit` display mode). The `fill` display mode makes the component fill the entire container, potentially distorting it.

```tsx title="ImageRotarySwitchAdaptiveSize.tsx"
import { useState } from "react";
import { ImageRotarySwitch, OptionView } from "@cutoff/audio-ui-react";

export default function ImageRotarySwitchAdaptiveSize() {
  const [wideVal, setWideVal] = useState("medium");
  const [tallVal, setTallVal] = useState("medium");
  const [distortedVal, setDistortedVal] = useState("medium");

  return (
    <div className="flex gap-6 items-center flex-wrap">
      <div className="w-48 h-16 border rounded-lg p-2">
        <ImageRotarySwitch
          value={wideVal}
          onChange={(e) => setWideVal(e.value)}
          adaptiveSize={true}
          label="Wide"
          frameWidth={100}
          frameHeight={100}
          imageHref="/images/demo/knob-volume.png"
        >
          <OptionView value="low">Low</OptionView>
          <OptionView value="medium">Medium</OptionView>
          <OptionView value="high">High</OptionView>
        </ImageRotarySwitch>
      </div>
      <div className="w-24 h-48 border rounded-lg p-2">
        <ImageRotarySwitch
          value={tallVal}
          onChange={(e) => setTallVal(e.value)}
          adaptiveSize={true}
          label="Tall"
          frameWidth={100}
          frameHeight={100}
          imageHref="/images/demo/knob-volume.png"
        >
          <OptionView value="low">Low</OptionView>
          <OptionView value="medium">Medium</OptionView>
          <OptionView value="high">High</OptionView>
        </ImageRotarySwitch>
      </div>
      <div className="w-32 h-20 border rounded-lg p-2">
        <ImageRotarySwitch
          value={distortedVal}
          onChange={(e) => setDistortedVal(e.value)}
          adaptiveSize={true}
          displayMode="fill"
          label="Distorted"
          frameWidth={100}
          frameHeight={100}
          imageHref="/images/demo/knob-volume.png"
        >
          <OptionView value="low">Low</OptionView>
          <OptionView value="medium">Medium</OptionView>
          <OptionView value="high">High</OptionView>
        </ImageRotarySwitch>
      </div>
    </div>
  );
}
```

## Common Use Cases

### Volume Level Selector

```tsx title="VolumeLevelSelector.tsx"
import { useState } from "react";
import { ImageRotarySwitch, OptionView } from "@cutoff/audio-ui-react";

export default function VolumeLevelSelectorExample() {
  const [value, setValue] = useState(2);

  return (
    <ImageRotarySwitch
      value={value}
      label="Volume Level"
      size="large"
      frameWidth={100}
      frameHeight={100}
      imageHref="/images/demo/knob-volume.png"
      onChange={(e) => setValue(e.value)}
    >
      <OptionView value={0}>0</OptionView>
      <OptionView value={1}>1</OptionView>
      <OptionView value={2}>2</OptionView>
      <OptionView value={3}>3</OptionView>
      <OptionView value={4}>4</OptionView>
    </ImageRotarySwitch>
  );
}
```

### Tone Selector

```tsx title="ToneSelector.tsx"
import { useState } from "react";
import { ImageRotarySwitch, OptionView } from "@cutoff/audio-ui-react";

export default function ToneSelectorExample() {
  const [value, setValue] = useState("mid");

  return (
    <ImageRotarySwitch
      value={value}
      label="Tone"
      size="large"
      frameWidth={100}
      frameHeight={100}
      imageHref="/images/demo/knob-tone.png"
      onChange={(e) => setValue(e.value)}
    >
      <OptionView value="low">Low</OptionView>
      <OptionView value="mid">Mid</OptionView>
      <OptionView value="high">High</OptionView>
    </ImageRotarySwitch>
  );
}
```

### Multi-Position Selector

```tsx title="MultiPositionSelector.tsx"
import { useState } from "react";
import { ImageRotarySwitch, OptionView } from "@cutoff/audio-ui-react";

export default function MultiPositionSelectorExample() {
  const [value, setValue] = useState(5);

  return (
    <ImageRotarySwitch
      value={value}
      label="Selector"
      size="large"
      frameWidth={100}
      frameHeight={100}
      imageHref="/images/demo/knob-selector.png"
      onChange={(e) => setValue(e.value)}
    >
      {Array.from({ length: 10 }, (_, i) => (
        <OptionView key={i} value={i}>
          {i}
        </OptionView>
      ))}
    </ImageRotarySwitch>
  );
}
```

## Image Requirements

* **Format**: Any image format supported by browsers (PNG, JPG, SVG, etc.)
* **Dimensions**: The `frameWidth` and `frameHeight` props determine the viewBox dimensions
* **Aspect Ratio**: The image will be scaled to fit the viewBox while preserving aspect ratio
* **Rotation**: The image rotates around its center point, snapping to discrete positions

## Rotation Calculation

The rotation angle is calculated as:

* Option index maps to normalized position (0 to 1)
* Normalized position maps to rotation range based on `openness`
* `rotation` offset is added to align the image
* Positions snap to discrete option values

## Next Steps

* Explore [ImageKnob](https://cutoff.dev/audio-ui/docs/latest/components/raster/image-knob) for continuous rotary controls
* Learn about [ImageSwitch](https://cutoff.dev/audio-ui/docs/latest/components/raster/image-switch) for boolean switches
* Check out the [playground](https://playground.cutoff.dev) for interactive examples


---

# ImageSwitch

# ImageSwitch

> A boolean control that displays one of two images based on state, perfect for on/off switches and buttons with custom bitmap graphics.

ImageSwitch is a boolean control that displays one of two images based on the boolean value. It shares the same layout, parameter model, interaction, and accessibility as other controls (vector, raster, and control primitives); see the [Raster introduction](https://cutoff.dev/audio-ui/docs/latest/components/raster/introduction).

The image displayed is determined by the boolean value:

* `false` (normalized 0.0) displays `imageHrefFalse`
* `true` (normalized 1.0) displays `imageHrefTrue`

> **Note:** **No Theming Support**: This component does NOT support themable props (color, roundness, thickness) as visuals are entirely determined by the image content.

## Props

| Name           | Type                                                   | Default | Required | Description                                                                              |
| :------------- | :----------------------------------------------------- | :------ | :------- | :--------------------------------------------------------------------------------------- |
| value          | boolean                                                | -       | Yes      | Current value of the control                                                             |
| onChange       | (event: AudioControlEvent\<boolean>) => void           | -       | No       | Handler for value changes                                                                |
| latch          | boolean                                                | false   | No       | Whether the button operates in latch (toggle) mode or momentary mode                     |
| label          | string                                                 | -       | No       | Label displayed below the component                                                      |
| frameWidth     | number                                                 | -       | Yes      | Width of the viewBox (determines viewBox width)                                          |
| frameHeight    | number                                                 | -       | Yes      | Height of the viewBox (determines viewBox height)                                        |
| imageHrefFalse | string                                                 | -       | Yes      | URL to the image for false/off state                                                     |
| imageHrefTrue  | string                                                 | -       | Yes      | URL to the image for true/on state                                                       |
| adaptiveSize   | boolean                                                | false   | No       | Whether the component stretches to fill its container                                    |
| size           | "xsmall" \| "small" \| "normal" \| "large" \| "xlarge" | normal  | No       | Size of the component                                                                    |
| parameter      | BooleanParameter                                       | -       | No       | Audio Parameter definition (Model). If provided, overrides label/latch from ad-hoc props |

## Basic Usage

ImageSwitch requires two image URLs (one for off state, one for on state) and frame dimensions:

```tsx title="BasicImageSwitch.tsx"
import { useState } from "react";
import { ImageSwitch } from "@cutoff/audio-ui-react";

export default function BasicImageSwitchExample() {
  const [isOn, setIsOn] = useState(false);

  return (
    <ImageSwitch
      value={isOn}
      latch={true}
      label="Power"
      size="large"
      frameWidth={100}
      frameHeight={100}
      imageHrefFalse="/images/demo/star-off.png"
      imageHrefTrue="/images/demo/star-on.png"
      onChange={(e) => setIsOn(e.value)}
    />
  );
}
```

## Modes of Operation

### Latch Mode (Toggle)

In latch mode, the button toggles between pressed and released states:

```tsx title="LatchImageSwitch.tsx"
import { useState } from "react";
import { ImageSwitch } from "@cutoff/audio-ui-react";

export default function LatchImageSwitchExample() {
  const [isOn, setIsOn] = useState(false);

  return (
    <ImageSwitch
      value={isOn}
      latch={true}
      label="Favorite"
      size="large"
      frameWidth={100}
      frameHeight={100}
      imageHrefFalse="/images/demo/star-off.png"
      imageHrefTrue="/images/demo/star-on.png"
      onChange={(e) => setIsOn(e.value)}
    />
  );
}
```

### Momentary Mode

In momentary mode, the button is only active while being pressed:

```tsx title="MomentaryImageSwitch.tsx"
import { useState } from "react";
import { ImageSwitch } from "@cutoff/audio-ui-react";

export default function MomentaryImageSwitchExample() {
  const [isPressed, setIsPressed] = useState(false);

  return (
    <ImageSwitch
      value={isPressed}
      latch={false}
      label="Record"
      size="large"
      frameWidth={100}
      frameHeight={100}
      imageHrefFalse="/images/demo/star-off.png"
      imageHrefTrue="/images/demo/star-on.png"
      onChange={(e) => setIsPressed(e.value)}
    />
  );
}
```

## Image Selection

The component displays different images based on the boolean value:

```tsx title="ImageSelection.tsx"
import { useState } from "react";
import { ImageSwitch } from "@cutoff/audio-ui-react";

export default function ImageSelectionExample() {
  const [isOn, setIsOn] = useState(false);

  return (
    <ImageSwitch
      value={isOn}
      latch={true}
      label="Switch"
      size="large"
      frameWidth={100}
      frameHeight={100}
      imageHrefFalse="/images/demo/star-off.png"
      imageHrefTrue="/images/demo/star-on.png"
      onChange={(e) => setIsOn(e.value)}
    />
  );
}
```

## Parameter Model

Use a parameter model for integration with audio parameter systems. The following example uses ad-hoc props; with the full library you would use `createBooleanParameter` and the `parameter` prop:

```tsx title="ImageSwitchParameterModel.tsx"
import { useState } from "react";
import { ImageSwitch } from "@cutoff/audio-ui-react";

export default function ImageSwitchParameterModelExample() {
  const [value, setValue] = useState(false);

  return (
    <ImageSwitch
      label="Favorite"
      latch={true}
      value={value}
      size="large"
      frameWidth={100}
      frameHeight={100}
      imageHrefFalse="/images/demo/star-off.png"
      imageHrefTrue="/images/demo/star-on.png"
      onChange={(e) => setValue(e.value)}
    />
  );
}
```

## Adaptive Sizing

The ImageSwitch component supports both fixed sizes and adaptive sizing. By default, the size of the component is driven by the [Size System](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/size-system) and their `size` attribute:

```tsx title="ImageSwitchSizing.tsx"
import { useState } from "react";
import { ImageSwitch } from "@cutoff/audio-ui-react";

export default function ImageSwitchSizingExample() {
  const [v1, setV1] = useState(false);
  const [v2, setV2] = useState(false);
  const [v3, setV3] = useState(false);
  const [v4, setV4] = useState(false);

  return (
    <div className="flex gap-4 items-center flex-wrap">
      <ImageSwitch
        value={v1}
        latch={true}
        label="Small"
        size="small"
        frameWidth={100}
        frameHeight={100}
        imageHrefFalse="/images/demo/star-off.png"
        imageHrefTrue="/images/demo/star-on.png"
        onChange={(e) => setV1(e.value)}
      />
      <ImageSwitch
        value={v2}
        latch={true}
        label="Normal"
        size="normal"
        frameWidth={100}
        frameHeight={100}
        imageHrefFalse="/images/demo/star-off.png"
        imageHrefTrue="/images/demo/star-on.png"
        onChange={(e) => setV2(e.value)}
      />
      <ImageSwitch
        value={v3}
        latch={true}
        label="Large"
        size="large"
        frameWidth={100}
        frameHeight={100}
        imageHrefFalse="/images/demo/star-off.png"
        imageHrefTrue="/images/demo/star-on.png"
        onChange={(e) => setV3(e.value)}
      />
      <ImageSwitch
        value={v4}
        latch={true}
        label="XLarge"
        size="xlarge"
        frameWidth={100}
        frameHeight={100}
        imageHrefFalse="/images/demo/star-off.png"
        imageHrefTrue="/images/demo/star-on.png"
        onChange={(e) => setV4(e.value)}
      />
    </div>
  );
}
```

Adaptive sizing allows the component to adapt to the size of its parent container, adjusting its dimensions intelligently without causing distortion (`scaleToFit` display mode). The `fill` display mode makes the component fill the entire container, potentially distorting it.

```tsx title="ImageSwitchAdaptiveSize.tsx"
import { useState } from "react";
import { ImageSwitch } from "@cutoff/audio-ui-react";

export default function ImageSwitchAdaptiveSize() {
  const [wideVal, setWideVal] = useState(false);
  const [tallVal, setTallVal] = useState(false);
  const [distortedVal, setDistortedVal] = useState(false);

  return (
    <div className="flex gap-6 items-center flex-wrap">
      <div className="w-32 h-12 border rounded-lg p-2">
        <ImageSwitch
          value={wideVal}
          onChange={(e) => setWideVal(e.value)}
          latch={true}
          adaptiveSize={true}
          label="Wide"
          frameWidth={100}
          frameHeight={100}
          imageHrefFalse="/images/demo/star-off.png"
          imageHrefTrue="/images/demo/star-on.png"
        />
      </div>
      <div className="w-12 h-32 border rounded-lg p-2">
        <ImageSwitch
          value={tallVal}
          onChange={(e) => setTallVal(e.value)}
          latch={true}
          adaptiveSize={true}
          label="Tall"
          frameWidth={100}
          frameHeight={100}
          imageHrefFalse="/images/demo/star-off.png"
          imageHrefTrue="/images/demo/star-on.png"
        />
      </div>
      <div className="w-24 h-20 border rounded-lg p-2">
        <ImageSwitch
          value={distortedVal}
          onChange={(e) => setDistortedVal(e.value)}
          latch={true}
          adaptiveSize={true}
          displayMode="fill"
          label="Distorted"
          frameWidth={100}
          frameHeight={100}
          imageHrefFalse="/images/demo/star-off.png"
          imageHrefTrue="/images/demo/star-on.png"
        />
      </div>
    </div>
  );
}
```

## Common Use Cases

### Favorite/Star Button

```tsx title="FavoriteButton.tsx"
import { useState } from "react";
import { ImageSwitch } from "@cutoff/audio-ui-react";

export default function FavoriteButtonExample() {
  const [isFavorite, setIsFavorite] = useState(false);

  return (
    <ImageSwitch
      value={isFavorite}
      latch={true}
      label="Favorite"
      size="large"
      frameWidth={100}
      frameHeight={100}
      imageHrefFalse="/images/demo/star-off.png"
      imageHrefTrue="/images/demo/star-on.png"
      onChange={(e) => setIsFavorite(e.value)}
    />
  );
}
```

### Power Switch

```tsx title="PowerSwitch.tsx"
import { useState } from "react";
import { ImageSwitch } from "@cutoff/audio-ui-react";

export default function PowerSwitchExample() {
  const [isOn, setIsOn] = useState(false);

  return (
    <ImageSwitch
      value={isOn}
      latch={true}
      label="Power"
      size="large"
      frameWidth={100}
      frameHeight={100}
      imageHrefFalse="/images/demo/star-off.png"
      imageHrefTrue="/images/demo/star-on.png"
      onChange={(e) => setIsOn(e.value)}
    />
  );
}
```

### Momentary Record Button

```tsx title="RecordButton.tsx"
import { useState } from "react";
import { ImageSwitch } from "@cutoff/audio-ui-react";

export default function RecordButtonExample() {
  const [isRecording, setIsRecording] = useState(false);

  return (
    <ImageSwitch
      value={isRecording}
      latch={false}
      label="Record"
      size="large"
      frameWidth={100}
      frameHeight={100}
      imageHrefFalse="/images/demo/star-off.png"
      imageHrefTrue="/images/demo/star-on.png"
      onChange={(e) => setIsRecording(e.value)}
    />
  );
}
```

## Image Requirements

* **Format**: Any image format supported by browsers (PNG, JPG, SVG, etc.)
* **Dimensions**: The `frameWidth` and `frameHeight` props determine the viewBox dimensions
* **Aspect Ratio**: Images will be scaled to fit the viewBox while preserving aspect ratio
* **States**: Provide two separate images for off and on states

## Image Selection Logic

The component selects images based on the boolean value:

* `value === false` → Displays `imageHrefFalse`
* `value === true` → Displays `imageHrefTrue`

The selection is immediate and does not animate between states.

## Next Steps

* Explore [ImageKnob](https://cutoff.dev/audio-ui/docs/latest/components/raster/image-knob) for continuous rotary controls
* Learn about [ImageRotarySwitch](https://cutoff.dev/audio-ui/docs/latest/components/raster/image-rotary-switch) for discrete rotary controls
* Check out the [playground](https://playground.cutoff.dev) for interactive examples


---

# Default Components Styling

# Default Components Styling

> Learn how to style AudioUI's default components using the built-in theming system, CSS variables, and theme management utilities.

AudioUI's default components (Knob, Slider, Button, CycleButton, Keys) use a CSS variable-based theming system optimized for realtime audio applications. This system provides a clean, idiomatic way to customize component colors and styles without React Context or JavaScript overhead. For a high-level overview of theming on vector components, see [Theming Features](https://cutoff.dev/audio-ui/docs/latest/components/vector/introduction#theming-features) in the Vector introduction.

**Note**: This styling system applies to AudioUI's built-in default components. Fully customized components built with [Control Primitives](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/control-primitives) and custom view components use their own independent styling and theming systems.

## Why a CSS Variable System?

Audio applications have strict performance requirements. The CSS variable approach ensures:

* **Zero React overhead**: Theme changes don't trigger re-renders
* **Automatic updates**: CSS handles theme changes instantly
* **Dark mode support**: Automatic adaptation via CSS `.dark` class
* **Animation support**: Colors can be animated smoothly
* **Framework agnostic**: Works with any CSS-capable framework

## How It Works

The color system uses a three-tier resolution hierarchy:

1. **Component prop** (`color` prop on individual components)
2. **Global CSS variable** (`--audioui-primary-color`)
3. **Adaptive default** (`--audioui-adaptive-default-color` - white in dark mode, black in light mode)

When you set a color, AudioUI automatically computes variants (`primary50`, `primary20`) using CSS `color-mix()`. These variants are used for layered effects, borders, and visual depth.

## Basic Usage

### Setting Global Theme

Use the theme utility functions to set a global theme that applies to all components:

```tsx title="GlobalTheme.tsx" showLineNumbers
import { useEffect } from "react";
import { setThemeColor, setThemeRoundness } from "@cutoff/audio-ui-react";
import { Knob, Button } from "@cutoff/audio-ui-react";

export default function GlobalThemeExample() {
  useEffect(() => {
    // Set global theme (affects all components)
    setThemeColor("#3b82f6"); // Blue
    setThemeRoundness(0.3); // Slightly rounded
  }, []);

  return (
    <div style={{ display: "flex", gap: "20px" }}>
      <Knob value={0.5} onChange={(e) => {}} label="Cutoff" />
      <Button value={false} onChange={(e) => {}} label="Power" />
    </div>
  );
}
```

### Using Predefined Colors

AudioUI provides predefined theme colors for convenience:

```tsx title="PredefinedColors.tsx" showLineNumbers
import { useEffect } from "react";
import { setThemeColor, themeColors } from "@cutoff/audio-ui-react";
import { Knob } from "@cutoff/audio-ui-react";

export default function PredefinedColorsExample() {
  useEffect(() => {
    // Use predefined colors
    setThemeColor(themeColors.blue);
    // Or: setThemeColor(themeColors.purple);
    // Or: setThemeColor(themeColors.orange);
  }, []);

  return <Knob value={0.5} onChange={(e) => {}} label="Volume" />;
}
```

Available predefined colors: `default`, `blue`, `orange`, `pink`, `green`, `purple`, `yellow`.

### Per-Component Colors

Override the global theme for individual components:

```tsx title="PerComponentColors.tsx" showLineNumbers
import { Knob, Slider } from "@cutoff/audio-ui-react";

export default function PerComponentColorsExample() {
  return (
    <div style={{ display: "flex", gap: "20px" }}>
      <Knob
        value={0.5}
        onChange={(e) => {}}
        label="Cutoff"
        color="#3b82f6" // Blue
      />
      <Knob
        value={0.5}
        onChange={(e) => {}}
        label="Resonance"
        color="#ef4444" // Red
      />
      <Slider
        value={0.7}
        onChange={(e) => {}}
        label="Volume"
        color="hsl(160, 95%, 44%)" // Green
      />
    </div>
  );
}
```

## Color Formats

The `color` prop accepts any valid CSS color value:

```tsx title="ColorFormats.tsx" showLineNumbers
import { Knob } from "@cutoff/audio-ui-react";

export default function ColorFormatsExample() {
  return (
    <div style={{ display: "flex", gap: "20px", flexWrap: "wrap" }}>
      <Knob value={0.5} onChange={(e) => {}} label="Named" color="blue" />
      <Knob value={0.5} onChange={(e) => {}} label="Hex" color="#FF5500" />
      <Knob
        value={0.5}
        onChange={(e) => {}}
        label="RGB"
        color="rgb(255, 85, 0)"
      />
      <Knob
        value={0.5}
        onChange={(e) => {}}
        label="HSL"
        color="hsl(20, 100%, 50%)"
      />
      <Knob
        value={0.5}
        onChange={(e) => {}}
        label="CSS Variable"
        color="var(--my-custom-color)"
      />
    </div>
  );
}
```

## Color Variants

AudioUI automatically generates color variants from your primary color:

* **`primary50`**: 50% opacity variant (for backgrounds, tracks)
* **`primary20`**: 20% opacity variant (for subtle effects)

These variants are computed using CSS `color-mix()`, so they update automatically when the primary color changes. Components use these variants for layered visual effects:

```tsx title="ColorVariants.tsx" showLineNumbers
import { Knob, Slider } from "@cutoff/audio-ui-react";

export default function ColorVariantsExample() {
  return (
    <div style={{ display: "flex", gap: "20px" }}>
      {/* Knob uses primary50 for background arc, primary for foreground */}
      <Knob value={0.5} onChange={(e) => {}} label="Knob" color="#3b82f6" />
      {/* Slider uses primary50 for track, primary for fill */}
      <Slider
        value={0.7}
        onChange={(e) => {}}
        label="Slider"
        color="#3b82f6"
        orientation="vertical"
      />
    </div>
  );
}
```

## Adaptive Default Colors

When no color is specified, components use an adaptive default that automatically switches between light and dark modes:

* **Light mode**: Near-black (`hsl(0, 0%, 10%)`)
* **Dark mode**: Near-white (`hsl(0, 0%, 96%)`)

This ensures components are always visible regardless of your application's theme:

```tsx title="AdaptiveDefault.tsx" showLineNumbers
import { Knob } from "@cutoff/audio-ui-react";

export default function AdaptiveDefaultExample() {
  // No color prop - uses adaptive default
  return <Knob value={0.5} onChange={(e) => {}} label="Adaptive" />;
}
```

## CSS-Only Customization

You can customize themes using pure CSS without JavaScript:

```tsx title="CSSOnlyTheme.tsx" showLineNumbers
export default function CSSOnlyThemeExample() {
  return (
    <>
      <style>{`
        .my-theme {
          --audioui-primary-color: hsl(280, 80%, 60%);
          --audioui-roundness-base: 0.5;
        }
      `}</style>
      <div className="my-theme">
        <Knob value={0.5} onChange={(e) => {}} label="Custom Theme" />
      </div>
    </>
  );
}
```

Or using inline styles:

```tsx title="InlineCSSTheme.tsx" showLineNumbers
import { Knob } from "@cutoff/audio-ui-react";

export default function InlineCSSThemeExample() {
  return (
    <div
      style={{
        "--audioui-primary-color": "hsl(200, 100%, 50%)",
        "--audioui-roundness-base": "0.4",
      } as React.CSSProperties}
    >
      <Knob value={0.5} onChange={(e) => {}} label="Inline Theme" />
    </div>
  );
}
```

## Roundness

Control the roundness of component corners using the `roundness` prop or global theme:

```tsx title="Roundness.tsx" showLineNumbers
import { Knob, Button } from "@cutoff/audio-ui-react";

export default function RoundnessExample() {
  return (
    <div style={{ display: "flex", gap: "20px" }}>
      <Knob
        value={0.5}
        onChange={(e) => {}}
        label="Square"
        roundness={0.0}
      />
      <Knob
        value={0.5}
        onChange={(e) => {}}
        label="Rounded"
        roundness={0.5}
      />
      <Knob
        value={0.5}
        onChange={(e) => {}}
        label="Fully Rounded"
        roundness={1.0}
      />
    </div>
  );
}
```

Set global roundness:

```tsx title="GlobalRoundness.tsx" showLineNumbers
import { useEffect } from "react";
import { setThemeRoundness } from "@cutoff/audio-ui-react";

export default function GlobalRoundnessExample() {
  useEffect(() => {
    setThemeRoundness(0.3); // Applies to all components
  }, []);

  return <Knob value={0.5} onChange={(e) => {}} label="Themed" />;
}
```

## Dynamic Theme Switching

Themes can be changed dynamically at runtime:

```tsx title="DynamicTheme.tsx" showLineNumbers
import { useState } from "react";
import { setThemeColor, themeColors } from "@cutoff/audio-ui-react";
import { Knob } from "@cutoff/audio-ui-react";

export default function DynamicThemeExample() {
  const [currentTheme, setCurrentTheme] = useState(themeColors.blue);

  const themes = [
    { name: "Blue", color: themeColors.blue },
    { name: "Purple", color: themeColors.purple },
    { name: "Orange", color: themeColors.orange },
    { name: "Green", color: themeColors.green },
  ];

  const handleThemeChange = (color: string) => {
    setThemeColor(color);
    setCurrentTheme(color);
  };

  return (
    <div>
      <div style={{ display: "flex", gap: "10px", marginBottom: "20px" }}>
        {themes.map((theme) => (
          <button
            key={theme.name}
            onClick={() => handleThemeChange(theme.color)}
            style={{
              padding: "8px 16px",
              backgroundColor:
                currentTheme === theme.color ? theme.color : "#e5e7eb",
              color: currentTheme === theme.color ? "white" : "black",
              border: "none",
              borderRadius: "4px",
              cursor: "pointer",
            }}
          >
            {theme.name}
          </button>
        ))}
      </div>
      <Knob value={0.5} onChange={(e) => {}} label="Themed Knob" />
    </div>
  );
}
```

## Color Resolution in Practice

Understanding the resolution hierarchy helps when debugging theme issues:

1. **Component prop takes precedence**: If you pass `color="red"` to a Knob, it uses red regardless of global theme
2. **Global theme is fallback**: If no `color` prop, components use `--audioui-primary-color`
3. **Adaptive default is final fallback**: If no global theme is set, components use the adaptive default

```tsx title="ResolutionExample.tsx" showLineNumbers
import { useEffect } from "react";
import { setThemeColor, themeColors } from "@cutoff/audio-ui-react";
import { Knob } from "@cutoff/audio-ui-react";

export default function ResolutionExample() {
  useEffect(() => {
    setThemeColor(themeColors.blue); // Global theme
  }, []);

  return (
    <div style={{ display: "flex", gap: "20px" }}>
      {/* Uses global blue theme */}
      <Knob value={0.5} onChange={(e) => {}} label="Global Theme" />
      {/* Overrides with red */}
      <Knob
        value={0.5}
        onChange={(e) => {}}
        label="Override"
        color="red"
      />
    </div>
  );
}
```

## Scope: Default Components Only

This styling system applies exclusively to AudioUI's default components:

* **Knob** - All variants (abstract, simplest, plainCap, iconCap)
* **Slider** - All variants (abstract, trackless, trackfull, stripless)
* **Button** - All variants
* **CycleButton** - All variants
* **Keys** - All key styles

**Custom components** built with [Control Primitives](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/control-primitives) and custom view components do not use this styling system. They implement their own styling and theming independently.

## Best Practices

1. **Set global theme once**: Use `setThemeColor()` in your app's root component or layout
2. **Use predefined colors**: `themeColors` provides consistent, tested color values
3. **Override sparingly**: Use per-component colors only when needed for specific UI elements
4. **CSS variables for scoped themes**: Use CSS variables when you need theme scoping (e.g., different themes for different sections)
5. **Test in both modes**: Always verify your colors work in both light and dark modes

## Next Steps

* Learn about [Default Components Sizing](https://cutoff.dev/audio-ui/docs/latest/customization/default-components-sizing) for component sizing
* Explore [AdaptiveBox](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/adaptive-box-layout) for layout and responsive behavior
* Explore [Audio Parameters](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/audio-parameters) for parameter-based theming
* Check out [Control Primitives](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/control-primitives) for building custom components with independent styling
* See component-specific documentation for advanced styling options


---

# Default Components Sizing

# Default Components Sizing

> Learn how to size AudioUI's default components using the built-in size system, from base units to multipliers, aspect ratios, and fixed vs adaptive sizing.

AudioUI's default components (Knob, Slider, Button, CycleButton, Keys) use a base unit system for consistent sizing. This system ensures mathematical harmony between components and provides both fixed and adaptive sizing options.

**Note**: This sizing system applies to AudioUI's built-in default components. Fully customized components built with [Control Primitives](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/control-primitives) and custom view components use their own independent sizing systems.

## Overview

The size system provides:

* **Consistent sizing**: All components derive from a single base unit
* **Mathematical harmony**: Components align perfectly in layouts
* **Flexible sizing**: Choose between fixed sizes or adaptive container-filling
* **Themeable**: Scale the entire system by changing the base unit

## Base Unit System

All component sizes derive from a single base unit:

* **Base Unit**: `--audioui-unit` (default: 48px)
* **Size Multipliers**:
  * `xsmall`: 1x base unit (48px)
  * `small`: 1.25x base unit (60px)
  * `normal`: 1.5x base unit (72px) - default
  * `large`: 2x base unit (96px)
  * `xlarge`: 2.5x base unit (120px)

### CSS Variable Structure

Size dimensions are defined using CSS variables:

```css
/* Base unit */
--audioui-unit: 48px;

/* Size multipliers */
--audioui-size-mult-xsmall: 1;
--audioui-size-mult-small: 1.25;
--audioui-size-mult-normal: 1.5;
--audioui-size-mult-large: 2;
--audioui-size-mult-xlarge: 2.5;

/* Square components (Button, Knob, CycleButton) */
--audioui-size-square-{size}: calc(var(--audioui-unit) * var(--audioui-size-mult-{size}));

/* Horizontal Slider (1x2) */
--audioui-size-hslider-height-{size}: calc(var(--audioui-unit) * multiplier);
--audioui-size-hslider-width-{size}: calc(height * 2);

/* Vertical Slider (2x1) */
--audioui-size-vslider-width-{size}: calc(var(--audioui-unit) * multiplier);
--audioui-size-vslider-height-{size}: calc(width * 2);

/* Keys (1x5) */
--audioui-size-keys-height-{size}: calc(var(--audioui-unit) * multiplier);
--audioui-size-keys-width-{size}: calc(height * 5);
```

## Component Aspect Ratios

Each component type has a fixed aspect ratio that defines its shape:

* **Button, Knob, CycleButton**: 1x1 (square)
* **Horizontal Slider**: 1x2 (width:height) - width > height
* **Vertical Slider**: 2x1 (width:height) - height > width
* **Keys**: 1x5 (width:height) - width > height

These aspect ratios are preserved at all sizes, ensuring components maintain their visual identity.

## Fixed Sizing

When `adaptiveSize={false}` (default), components use fixed sizes based on the `size` prop:

```tsx title="FixedSizing.tsx" showLineNumbers
import { Knob, Slider } from "@cutoff/audio-ui-react";

export default function FixedSizingExample() {
  return (
    <div style={{ display: "flex", gap: "20px", alignItems: "flex-end" }}>
      <Knob value={0.5} onChange={(e) => {}} label="Small" size="small" />
      <Knob value={0.5} onChange={(e) => {}} label="Normal" size="normal" />
      <Knob value={0.5} onChange={(e) => {}} label="Large" size="large" />
      <Slider
        value={0.5}
        onChange={(e) => {}}
        label="Slider"
        size="normal"
        orientation="horizontal"
      />
    </div>
  );
}
```

### How Fixed Sizing Works

1. **CSS Classes**: Size class is applied for semantic purposes and external styling
2. **Inline Styles**: Size dimensions are applied as inline styles (CSS variable references) to override AdaptiveBox's default 100% sizing
3. **Precedence**: User `className` and `style` props take precedence over size classes/styles

## Adaptive Sizing

When `adaptiveSize={true}`, components fill their container and ignore the `size` prop for layout constraints:

```tsx title="AdaptiveSizing.tsx" showLineNumbers
import { Knob } from "@cutoff/audio-ui-react";

export default function AdaptiveSizingExample() {
  return (
    <div
      style={{
        display: "grid",
        gridTemplateColumns: "1fr 1fr",
        gap: "20px",
        height: "200px",
      }}
    >
      <Knob
        value={0.5}
        onChange={(e) => {}}
        label="Adaptive"
        adaptiveSize={true}
      />
      <Knob
        value={0.5}
        onChange={(e) => {}}
        label="Adaptive"
        adaptiveSize={true}
      />
    </div>
  );
}
```

### How Adaptive Sizing Works

1. No size class or inline size styles are applied
2. AdaptiveBox's default `width: 100%; height: 100%` takes effect
3. Component fills its container while maintaining aspect ratio

## Size Props

All controls support a `size` prop and an `adaptiveSize` prop:

```typescript
type SizeType = "xsmall" | "small" | "normal" | "large" | "xlarge";

type AdaptiveSizeProps = {
  size?: SizeType; // default: "normal"
  adaptiveSize?: boolean; // default: false
};
```

## Customization

### Changing Base Unit

Scale the entire size system by modifying the base unit:

```css
:root {
  --audioui-unit: 60px; /* Increase from 48px to 60px */
}
```

All components will scale proportionally while maintaining their aspect ratios:

* `xsmall`: 60px (was 48px)
* `small`: 75px (was 60px)
* `normal`: 90px (was 72px)
* `large`: 120px (was 96px)
* `xlarge`: 150px (was 120px)

### Overriding Size

Users can override size constraints using `className` or `style` props. User-provided styles are spread after size styles, so they take precedence:

```tsx title="SizeOverride.tsx" showLineNumbers
import { Knob } from "@cutoff/audio-ui-react";

export default function SizeOverrideExample() {
  return (
    <div style={{ display: "flex", gap: "20px" }}>
      {/* CSS class override (if higher specificity) */}
      <Knob
        value={0.5}
        onChange={(e) => {}}
        label="CSS Override"
        size="normal"
        className="w-20 h-20"
      />
      {/* Inline style override (always wins) */}
      <Knob
        value={0.5}
        onChange={(e) => {}}
        label="Inline Override"
        size="normal"
        style={{ width: "100px", height: "100px" }}
      />
    </div>
  );
}
```

## Design System Consistency

The size system ensures mathematical harmony between components:

* A "small" knob (1x1) aligns perfectly with a "small" slider's track width
* All components use the same base unit and multipliers
* Size variations are consistent across component types
* Aspect ratios are preserved at all sizes

## Performance Considerations

* **CSS Variables**: Size classes use CSS variables (computed at render time, cached by browser)
* **No JavaScript Calculations**: All sizing is CSS-based
* **Optimized Merging**: Simple object spread for style merging (no `useMemo` overhead needed)
* **Memoization**: All controls are wrapped with `React.memo` - style objects are only created when props change

## Scope: Default Components Only

This sizing system applies exclusively to AudioUI's default components:

* **Knob** - All variants (abstract, simplest, plainCap, iconCap)
* **Slider** - All variants (abstract, trackless, trackfull, stripless)
* **Button** - All variants
* **CycleButton** - All variants
* **Keys** - All key styles

**Custom components** built with [Control Primitives](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/control-primitives) and custom view components do not use this sizing system. They implement their own sizing independently.

## Integration with AdaptiveBox

The size system works in conjunction with [AdaptiveBox](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/adaptive-box-layout):

* **Fixed Sizing**: Size dimensions override AdaptiveBox's default 100% sizing
* **Adaptive Sizing**: AdaptiveBox's container-filling behavior takes effect
* **Aspect Ratios**: Both systems respect component aspect ratios

## Next Steps

* Learn about [Default Components Styling](https://cutoff.dev/audio-ui/docs/latest/customization/default-components-styling) for theming default components
* Explore [AdaptiveBox](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/adaptive-box-layout) for layout and responsive behavior
* Check out [Control Primitives](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/control-primitives) for building custom components with independent sizing
* See component-specific documentation for size-related features


---

# Web Audio API

# Web Audio API

> Wire AudioUI controls to the native Web Audio API. Covers AudioContext lifecycle, user-gesture unlock, AudioParam automation, and a minimal polysynth example.

This guide shows how to bind AudioUI controls directly to the Web Audio API, the native browser audio stack that every higher-level audio library sits on top of. You'll build a small polysynth with a filter, master volume, waveform selector, and playable keys.

If you prefer a higher-level library, see the [Tone.js integration](https://cutoff.dev/audio-ui/docs/latest/integrations/tone-js). The patterns here transfer.

For a more complete implementation (per-voice filtering, full ADSR, panning, sustain pedal, hardware MIDI input), read the **playground app** source at [`apps/playground-react/app/examples/webaudio/`](https://github.com/cutoff/audio-ui/tree/main/apps/playground-react/app/examples/webaudio) in the AudioUI repository.

## What you'll build

A synth with:

* A waveform selector (`CycleButton` to oscillator type)
* A lowpass filter controlled by two knobs (cutoff + resonance)
* A master volume slider
* A piano keyboard (`Keys` to note-on / note-off)

## AudioContext lifecycle

Browsers suspend `AudioContext` until a user gesture. Create the context early but resume it on a click:

```tsx title="useAudioContext.ts" showLineNumbers
import { useEffect, useRef, useState } from "react";

export function useAudioContext() {
  const ctxRef = useRef<AudioContext | null>(null);
  const [ready, setReady] = useState(false);

  useEffect(() => {
    const ctx = new (window.AudioContext || (window as any).webkitAudioContext)();
    ctxRef.current = ctx;
    return () => {
      ctx.close();
    };
  }, []);

  const start = () => {
    if (ctxRef.current?.state === "suspended") {
      ctxRef.current.resume();
    }
    setReady(true);
  };

  return { ctx: ctxRef.current, ready, start };
}
```

## The synth engine

Keep DSP concerns outside React. A plain class that owns the node graph is easier to reason about than state in hooks. Parameters are passed in their real-world units (Hz, Q-factor, linear gain 0-1), matching what the AudioUI controls emit directly.

```ts title="SynthEngine.ts" showLineNumbers
export class SynthEngine {
  private ctx: AudioContext;
  private filter: BiquadFilterNode;
  private master: GainNode;
  private voices = new Map<number, { osc: OscillatorNode; amp: GainNode }>();
  private waveform: OscillatorType = "sawtooth";

  constructor(ctx: AudioContext) {
    this.ctx = ctx;

    this.filter = ctx.createBiquadFilter();
    this.filter.type = "lowpass";
    this.filter.frequency.value = 1000;
    this.filter.Q.value = 1;

    this.master = ctx.createGain();
    this.master.gain.value = 0.3;

    this.filter.connect(this.master);
    this.master.connect(ctx.destination);
  }

  private midiToHz(midi: number) {
    return 440 * Math.pow(2, (midi - 69) / 12);
  }

  noteOn(midi: number) {
    if (this.voices.has(midi)) return;
    const osc = this.ctx.createOscillator();
    const amp = this.ctx.createGain();
    osc.type = this.waveform;
    osc.frequency.value = this.midiToHz(midi);
    amp.gain.value = 0;
    osc.connect(amp);
    amp.connect(this.filter);
    osc.start();
    amp.gain.setTargetAtTime(0.5, this.ctx.currentTime, 0.01);
    this.voices.set(midi, { osc, amp });
  }

  noteOff(midi: number) {
    const voice = this.voices.get(midi);
    if (!voice) return;
    voice.amp.gain.setTargetAtTime(0, this.ctx.currentTime, 0.05);
    voice.osc.stop(this.ctx.currentTime + 0.5);
    this.voices.delete(midi);
  }

  // Accepts Hz directly. The Knob handles log-scale mapping via scale="log".
  setCutoff(hz: number) {
    this.filter.frequency.setTargetAtTime(hz, this.ctx.currentTime, 0.02);
  }

  setResonance(q: number) {
    this.filter.Q.setTargetAtTime(q, this.ctx.currentTime, 0.02);
  }

  setVolume(gain: number) {
    this.master.gain.setTargetAtTime(gain, this.ctx.currentTime, 0.02);
  }

  setWaveform(wave: OscillatorType) {
    this.waveform = wave;
  }
}
```

## Bridging AudioUI to the engine

`onChange` on AudioUI controls receives an `AudioControlEvent`. Extract `e.value` and pass it to the engine. Note how the controls work directly in real units (Hz for cutoff, linear for gain) and use `valueFormatter` / `valueAsLabel` to show the current value while the user is interacting:

```tsx title="Synth.tsx" showLineNumbers
import { useEffect, useRef, useState } from "react";
import { Knob, Slider, CycleButton, Keys, frequencyFormatter } from "@cutoff/audio-ui-react";
import "@cutoff/audio-ui-react/style.css";
import { SynthEngine } from "./SynthEngine";
import { useAudioContext } from "./useAudioContext";

export default function Synth() {
  const { ctx, ready, start } = useAudioContext();
  const engineRef = useRef<SynthEngine | null>(null);

  const [cutoff, setCutoff] = useState(1000);
  const [resonance, setResonance] = useState(1);
  const [volume, setVolume] = useState(0.3);
  const [waveform, setWaveform] = useState<OscillatorType>("sawtooth");

  useEffect(() => {
    if (!ctx) return;
    engineRef.current = new SynthEngine(ctx);
    engineRef.current.setCutoff(cutoff);
    engineRef.current.setResonance(resonance);
    engineRef.current.setVolume(volume);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [ctx]);

  return (
    <div className="dark">
      {!ready && <button onClick={start}>Start Audio</button>}

      <CycleButton
        value={waveform}
        onChange={(e) => {
          setWaveform(e.value);
          engineRef.current?.setWaveform(e.value);
        }}
        options={[
          { value: "sawtooth", label: "Saw" },
          { value: "square", label: "Square" },
          { value: "sine", label: "Sine" },
          { value: "triangle", label: "Tri" },
        ]}
        label="Wave"
      />

      <Knob
        label="Cutoff"
        value={cutoff}
        defaultValue={1000}
        min={20}
        max={10000}
        scale="log"
        valueFormatter={(v) => frequencyFormatter(v)}
        valueAsLabel="interactive"
        onChange={(e) => {
          setCutoff(e.value);
          engineRef.current?.setCutoff(e.value);
        }}
      />

      <Knob
        label="Q"
        value={resonance}
        defaultValue={1}
        min={0.1}
        max={20}
        step={0.1}
        valueFormatter={(v) => v.toFixed(1)}
        valueAsLabel="interactive"
        onChange={(e) => {
          setResonance(e.value);
          engineRef.current?.setResonance(e.value);
        }}
      />

      <Slider
        label="Vol"
        value={volume}
        defaultValue={0.3}
        min={0}
        max={1}
        step={0.01}
        orientation="vertical"
        valueFormatter={(v) => `${Math.round(v * 100)}%`}
        valueAsLabel="interactive"
        onChange={(e) => {
          setVolume(e.value);
          engineRef.current?.setVolume(e.value);
        }}
      />

      <Keys
        nbKeys={25}
        startKey="C"
        octaveShift={0}
        onChange={(e) => {
          const { note, active } = e.value as { note: number; active: boolean };
          if (active) engineRef.current?.noteOn(note);
          else engineRef.current?.noteOff(note);
        }}
      />
    </div>
  );
}
```

## Key patterns

### `onChange` receives an event, not a raw value

For continuous and discrete controls (Knob, Slider, Button, CycleButton), `e.value` is the typed value.

For `Keys`, `e.value` is `{ note: number; active: boolean }`. Destructure it and branch on `active` for note-on / note-off.

### MIDI to frequency

Standard formula: `440 * 2^((midi - 69) / 12)`. MIDI note 69 is A4 = 440 Hz.

### Smooth parameter changes

Write to `AudioParam` via `setTargetAtTime(target, startTime, timeConstant)` rather than assigning `.value` directly. The time constant (in seconds) controls how fast the param reaches the target. Values of 0.01-0.05 avoid zippering on knob drags.

### Let AudioUI handle non-linear knob mapping

Filter cutoff, frequency, and anything else humans perceive logarithmically should use a log-scale Knob. Pass `scale="log"` and real-world `min` / `max` values (e.g. `min={20} max={10000}` for Hz), and the Knob handles the perceptual mapping internally. Your engine receives values in real units, no manual `Math.pow` conversion needed.

Pair this with `valueFormatter={(v) => frequencyFormatter(v)}` to display the value in context (Hz, kHz) and `valueAsLabel="interactive"` to swap the label to the live value while the user is dragging.

### `defaultValue` enables reset

When `defaultValue` is set, double-clicking the control resets it to that value. This is the standard DAW/plugin reset gesture. Always set it to a sensible starting value.

### Keep DSP outside React

The `SynthEngine` class owns the `AudioContext` and nodes. React only holds state for UI rendering and forwards changes to the engine via `useRef`. That keeps per-frame re-renders from interfering with audio.

## Going further

The example above is deliberately minimal. The [playground app's `webaudio` example](https://github.com/cutoff/audio-ui/tree/main/apps/playground-react/app/examples/webaudio) shows a full implementation with:

* Per-voice filtering (each note gets its own `BiquadFilter` for independent resonance)
* Full ADSR envelope (`linearRampToValueAtTime` for attack/decay, sustain level, exponential release)
* Stereo panning via `StereoPannerNode`
* Sustain pedal (latch `Button` that defers note-offs until released)
* Live parameter updates across active voices (knob drags affect currently-held notes)
* Hardware MIDI input via the Web MIDI API

It's the canonical reference if you're building a real synth interface with AudioUI.

## Related

* [Tone.js integration](https://cutoff.dev/audio-ui/docs/latest/integrations/tone-js), a higher-level synth/effects library
* [Audio Parameters](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/audio-parameters), AudioUI's parameter model for strict value validation
* [Interaction System](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/interaction-system), keyboard, wheel, touch input


---

# Tone.js

# Tone.js

> Wire AudioUI controls to Tone.js — a higher-level audio framework with synths, effects, and transport. Covers PolySynth, filter routing, and MIDI-to-note-name bridging.

[Tone.js](https://tonejs.github.io/) is a higher-level abstraction over the Web Audio API with built-in synths, effects, transports, and musical scheduling. This guide shows how to bind AudioUI controls to a Tone.js polysynth.

If you need lower-level control, the [Web Audio API integration](https://cutoff.dev/audio-ui/docs/latest/integrations/web-audio-api) covers the same patterns against the raw browser API.

## Install

```bash
npm install tone
```

Tone.js ships its own TypeScript types.

## What you'll build

A polysynth driven by AudioUI controls:

* `CycleButton` → oscillator waveform
* `Knob` → filter cutoff
* `Slider` → master volume
* `Keys` → polyphonic note-on / note-off

## The synth

Build the Tone graph once and keep it in a ref:

```tsx title="Synth.tsx" showLineNumbers
import { useEffect, useRef, useState } from "react";
import * as Tone from "tone";
import { Knob, Slider, CycleButton, Keys } from "@cutoff/audio-ui-react";
import "@cutoff/audio-ui-react/style.css";

type Waveform = "sawtooth" | "square" | "sine" | "triangle";

export default function Synth() {
  const synthRef = useRef<Tone.PolySynth | null>(null);
  const filterRef = useRef<Tone.Filter | null>(null);
  const masterRef = useRef<Tone.Gain | null>(null);
  const [ready, setReady] = useState(false);

  const [cutoff, setCutoff] = useState(60);
  const [volume, setVolume] = useState(60);
  const [waveform, setWaveform] = useState<Waveform>("sawtooth");

  useEffect(() => {
    const filter = new Tone.Filter(1200, "lowpass");
    const master = new Tone.Gain(0.6);
    const synth = new Tone.PolySynth(Tone.Synth, {
      oscillator: { type: "sawtooth" },
      envelope: { attack: 0.01, decay: 0.1, sustain: 0.6, release: 0.3 },
    });

    synth.chain(filter, master, Tone.getDestination());

    synthRef.current = synth;
    filterRef.current = filter;
    masterRef.current = master;

    return () => {
      synth.dispose();
      filter.dispose();
      master.dispose();
    };
  }, []);

  // Tone.start() requires a user gesture — browsers suspend AudioContext otherwise
  const startAudio = async () => {
    await Tone.start();
    setReady(true);
  };

  // Filter cutoff — map Knob's linear 0..100 to 80 Hz .. 10 kHz exponentially
  const handleCutoff = (e: { value: number }) => {
    setCutoff(e.value);
    const min = 80, max = 10000;
    const hz = min * Math.pow(max / min, e.value / 100);
    filterRef.current?.frequency.rampTo(hz, 0.02);
  };

  const handleVolume = (e: { value: number }) => {
    setVolume(e.value);
    masterRef.current?.gain.rampTo(e.value / 100, 0.02);
  };

  const handleWaveform = (e: { value: Waveform }) => {
    setWaveform(e.value);
    synthRef.current?.set({ oscillator: { type: e.value } });
  };

  // Keys emits { note, active } — convert MIDI to note name for Tone
  const handleKeys = (e: { value: { note: number; active: boolean } }) => {
    const { note, active } = e.value;
    const pitch = Tone.Frequency(note, "midi").toNote();
    if (active) synthRef.current?.triggerAttack(pitch);
    else synthRef.current?.triggerRelease(pitch);
  };

  return (
    <div className="dark">
      {!ready && <button onClick={startAudio}>Start Audio</button>}

      <CycleButton
        value={waveform}
        onChange={handleWaveform}
        options={[
          { value: "sawtooth", label: "Saw" },
          { value: "square", label: "Square" },
          { value: "sine", label: "Sine" },
          { value: "triangle", label: "Tri" },
        ]}
        label="Wave"
      />

      <Knob value={cutoff} min={0} max={100} onChange={handleCutoff} label="Cutoff" />

      <Slider
        value={volume}
        min={0}
        max={100}
        orientation="vertical"
        onChange={handleVolume}
        label="Vol"
      />

      <Keys nbKeys={25} startKey="C" octaveShift={0} onChange={handleKeys} />
    </div>
  );
}
```

## Key patterns

### `Tone.start()` gates audio on user gesture

Tone.js internally manages `AudioContext`. Call `Tone.start()` on a user click/tap before triggering sound — otherwise notes are silent. See [Tone's AudioContext docs](https://tonejs.github.io/docs/14.7.39/Context).

### MIDI to note name

`Tone.Frequency(midi, "midi").toNote()` converts a MIDI integer (what AudioUI's Keys emits) to a Tone-compatible pitch string (`"C4"`, `"A#3"`). Use this every time you bridge Keys to Tone.

Alternative: pass the MIDI number directly to `triggerAttack` as a frequency. Tone accepts both.

### `PolySynth` for polyphonic notes

A bare `Tone.Synth` is monophonic. For the keyboard to hold multiple notes, wrap it in `Tone.PolySynth(Tone.Synth, {...})`. Triggering the same pitch twice is idempotent.

### Smooth parameter changes via `rampTo`

Tone's `Signal.rampTo(value, time)` is equivalent to the Web Audio `setTargetAtTime` pattern — use it for knob-driven params to avoid zipper noise.

### Effects chain

Add effects inline via `synth.chain(...)`. For example, to add reverb and delay:

```ts
const reverb = new Tone.Reverb({ decay: 2, wet: 0.3 });
const delay = new Tone.FeedbackDelay({ delayTime: "8n", feedback: 0.3 });
synth.chain(filter, delay, reverb, master, Tone.getDestination());
```

Bind AudioUI knobs to `reverb.wet.value`, `delay.feedback.value`, etc.

### `Tone.Transport` vs direct triggers

For free-form playing (a keyboard), trigger directly. For scheduled sequences (step sequencer, arpeggiator), use `Tone.Transport` with `Tone.Pattern` or `Tone.Sequence`. The keyboard example above uses direct triggers.

## Related

* [Web Audio API integration](https://cutoff.dev/audio-ui/docs/latest/integrations/web-audio-api) — lower-level approach
* [Audio Parameters](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/audio-parameters) — AudioUI's parameter model
* [Keys component](https://cutoff.dev/audio-ui/docs/latest/components/vector/keys) — keyboard API reference


---

# AdaptiveBox

# AdaptiveBox

> Complete reference for AdaptiveBox component, the layout system that powers all AudioUI controls.

AdaptiveBox is the layout component that powers all AudioUI controls. It provides a CSS-only layout system for SVG-based controls with optional labels, ensuring responsive behavior without JavaScript overhead.

## Overview

AdaptiveBox solves the layout challenge of combining SVG graphics with text labels in a responsive, container-aware way. It uses CSS container queries and aspect-ratio calculations to create a scalable layout system.

### Key Features

* **Pure CSS layout**: No ResizeObserver or JavaScript calculations
* **Container queries**: Responsive sizing using `cqw`/`cqh` units
* **Aspect-preserving scaling**: Maintains component proportions
* **Label integration**: Seamless label positioning and alignment
* **Performance optimized**: Zero JavaScript overhead for layout

## Architecture

AdaptiveBox uses a three-layer structure:

1. **Wrapper**: Container query provider (`container-type: size`)
2. **Aspect Scaler**: Preserves combined aspect ratio (SVG + label)
3. **Content**: SVG and label in a two-row grid

### DOM Structure

```tsx
<AdaptiveBox viewBoxWidth={100} viewBoxHeight={100} labelHeightUnits={15}>
  <AdaptiveBox.Svg>
    {/* SVG content */}
  </AdaptiveBox.Svg>
  <AdaptiveBox.Label>Label Text</AdaptiveBox.Label>
</AdaptiveBox>
```

## Props Reference

### AdaptiveBox Props

| Name             | Type                            | Default    | Required | Description                                   |
| :--------------- | :------------------------------ | :--------- | :------- | :-------------------------------------------- |
| viewBoxWidth     | number                          | -          | Yes      | Width of the SVG viewBox (W)                  |
| viewBoxHeight    | number                          | -          | Yes      | Height of the SVG viewBox (H)                 |
| displayMode      | "scaleToFit" \| "fill"          | scaleToFit | No       | How the component scales within its container |
| labelMode        | "visible" \| "hidden" \| "none" | visible    | No       | Label display mode                            |
| labelHeightUnits | number                          | 15         | No       | Label height in viewBox units (L)             |
| minWidth         | string                          | -          | No       | CSS min-width for the wrapper                 |
| minHeight        | string                          | -          | No       | CSS min-height for the wrapper                |
| className        | string                          | -          | No       | Additional CSS classes                        |
| style            | React.CSSProperties             | -          | No       | Additional inline styles                      |

### AdaptiveBox.Svg Props

```typescript
type AdaptiveBoxSvgProps = {
  viewBoxWidth?: number; // Override from AdaptiveBox (rarely needed)
  viewBoxHeight?: number; // Override from AdaptiveBox (rarely needed)
  hAlign?: "start" | "center" | "end"; // Horizontal alignment
  vAlign?: "start" | "center" | "end"; // Vertical alignment
  preserveAspectRatio?: string; // SVG preserveAspectRatio (auto-set in fill mode)
  onWheel?: (e: WheelEvent) => void; // Wheel event handler
  onMouseDown?: (e: MouseEvent) => void; // Mouse down handler
  // ... other SVG event handlers
};
```

### AdaptiveBox.Label Props

```typescript
type AdaptiveBoxLabelProps = {
  position?: "above" | "below"; // Default: "below"
  align?: "start" | "center" | "end"; // Default: "center"
  labelMode?: "visible" | "hidden" | "none"; // Override from AdaptiveBox
  children?: React.ReactNode; // Label content
};
```

## Display Modes

### scaleToFit (Default)

The component scales to fit within its container while preserving aspect ratio. Letterboxing occurs on the non-limiting axis.

```tsx title="ScaleToFit.tsx" showLineNumbers
import { AdaptiveBox } from "@cutoff/audio-ui-react";

export default function ScaleToFitExample() {
  return (
    <div style={{ width: "200px", height: "150px", border: "1px solid #ccc" }}>
      <AdaptiveBox
        viewBoxWidth={100}
        viewBoxHeight={100}
        labelHeightUnits={15}
        displayMode="scaleToFit"
      >
        <AdaptiveBox.Svg>
          <circle cx="50" cy="50" r="40" fill="currentColor" />
        </AdaptiveBox.Svg>
        <AdaptiveBox.Label>Scale to Fit</AdaptiveBox.Label>
      </AdaptiveBox>
    </div>
  );
}
```

### fill

The component fills its container completely. The SVG may distort to fill the available space.

```tsx title="Fill.tsx" showLineNumbers
import { AdaptiveBox } from "@cutoff/audio-ui-react";

export default function FillExample() {
  return (
    <div style={{ width: "200px", height: "150px", border: "1px solid #ccc" }}>
      <AdaptiveBox
        viewBoxWidth={100}
        viewBoxHeight={100}
        labelHeightUnits={15}
        displayMode="fill"
      >
        <AdaptiveBox.Svg>
          <circle cx="50" cy="50" r="40" fill="currentColor" />
        </AdaptiveBox.Svg>
        <AdaptiveBox.Label>Fill Container</AdaptiveBox.Label>
      </AdaptiveBox>
    </div>
  );
}
```

## Label Modes

### visible (Default)

Label is rendered and visible. Space is reserved in the layout.

```tsx title="LabelVisible.tsx" showLineNumbers
<AdaptiveBox viewBoxWidth={100} viewBoxHeight={100} labelMode="visible">
  <AdaptiveBox.Svg>{/* SVG */}</AdaptiveBox.Svg>
  <AdaptiveBox.Label>Visible Label</AdaptiveBox.Label>
</AdaptiveBox>
```

### hidden

Label space is reserved but the label is not rendered (useful for consistent sizing).

```tsx title="LabelHidden.tsx" showLineNumbers
<AdaptiveBox viewBoxWidth={100} viewBoxHeight={100} labelMode="hidden">
  <AdaptiveBox.Svg>{/* SVG */}</AdaptiveBox.Svg>
  <AdaptiveBox.Label>Hidden Label</AdaptiveBox.Label>
</AdaptiveBox>
```

### none

No label space is reserved. Component uses SVG-only aspect ratio.

```tsx title="LabelNone.tsx" showLineNumbers
<AdaptiveBox viewBoxWidth={100} viewBoxHeight={100} labelMode="none">
  <AdaptiveBox.Svg>{/* SVG */}</AdaptiveBox.Svg>
  {/* No AdaptiveBox.Label needed */}
</AdaptiveBox>
```

## Label Positioning

### Position (Above/Below)

```tsx title="LabelPosition.tsx" showLineNumbers
import { AdaptiveBox } from "@cutoff/audio-ui-react";

export default function LabelPositionExample() {
  return (
    <div style={{ display: "flex", gap: "20px" }}>
      <AdaptiveBox viewBoxWidth={100} viewBoxHeight={100}>
        <AdaptiveBox.Svg>{/* SVG */}</AdaptiveBox.Svg>
        <AdaptiveBox.Label position="above">Above</AdaptiveBox.Label>
      </AdaptiveBox>
      <AdaptiveBox viewBoxWidth={100} viewBoxHeight={100}>
        <AdaptiveBox.Svg>{/* SVG */}</AdaptiveBox.Svg>
        <AdaptiveBox.Label position="below">Below</AdaptiveBox.Label>
      </AdaptiveBox>
    </div>
  );
}
```

### Alignment (Start/Center/End)

```tsx title="LabelAlignment.tsx" showLineNumbers
import { AdaptiveBox } from "@cutoff/audio-ui-react";

export default function LabelAlignmentExample() {
  return (
    <div style={{ display: "flex", gap: "20px" }}>
      <AdaptiveBox viewBoxWidth={100} viewBoxHeight={100}>
        <AdaptiveBox.Svg>{/* SVG */}</AdaptiveBox.Svg>
        <AdaptiveBox.Label align="start">Start</AdaptiveBox.Label>
      </AdaptiveBox>
      <AdaptiveBox viewBoxWidth={100} viewBoxHeight={100}>
        <AdaptiveBox.Svg>{/* SVG */}</AdaptiveBox.Svg>
        <AdaptiveBox.Label align="center">Center</AdaptiveBox.Label>
      </AdaptiveBox>
      <AdaptiveBox viewBoxWidth={100} viewBoxHeight={100}>
        <AdaptiveBox.Svg>{/* SVG */}</AdaptiveBox.Svg>
        <AdaptiveBox.Label align="end">End</AdaptiveBox.Label>
      </AdaptiveBox>
    </div>
  );
}
```

## Layout Algorithm

### Step 1: Wrapper Setup

The wrapper fills its parent and provides container query units:

```css
.wrapper {
  width: 100%;
  height: 100%;
  container-type: size; /* Enables cqw/cqh units */
}
```

### Step 2: Aspect Calculation

The aspect scaler calculates the combined aspect ratio:

```
Aspect Ratio = W / (H + L)
```

Where:

* `W` = viewBoxWidth
* `H` = viewBoxHeight
* `L` = labelHeightUnits

### Step 3: Scaling

In `scaleToFit` mode:

```css
.aspect-scaler {
  aspect-ratio: W / (H + L);
  width: min(100%, calc(100cqh * W / (H + L)));
  height: auto;
}
```

In `fill` mode:

```css
.aspect-scaler {
  width: 100%;
  height: 100%;
}
```

### Step 4: Grid Layout

The scaler uses a two-row grid:

```css
.grid-template-rows: H_percent% L_percent%;
```

Label below (default): SVG in row 1, Label in row 2
Label above: Label in row 1, SVG in row 2

## Alignment

### Scaler Alignment

Control how the scaler aligns within the wrapper:

```tsx title="ScalerAlignment.tsx" showLineNumbers
<AdaptiveBox
  viewBoxWidth={100}
  viewBoxHeight={100}
  style={{
    justifySelf: "start", // or "center", "end"
    alignSelf: "start", // or "center", "end"
  }}
>
  <AdaptiveBox.Svg>{/* SVG */}</AdaptiveBox.Svg>
  <AdaptiveBox.Label>Aligned</AdaptiveBox.Label>
</AdaptiveBox>
```

Or use `hAlign`/`vAlign` on `AdaptiveBox.Svg`:

```tsx title="SvgAlignment.tsx" showLineNumbers
<AdaptiveBox viewBoxWidth={100} viewBoxHeight={100}>
  <AdaptiveBox.Svg hAlign="start" vAlign="center">
    {/* SVG */}
  </AdaptiveBox.Svg>
  <AdaptiveBox.Label>Aligned</AdaptiveBox.Label>
</AdaptiveBox>
```

## Overlays

Add HTML content overlays above the SVG:

```tsx title="Overlay.tsx" showLineNumbers
import { AdaptiveBox } from "@cutoff/audio-ui-react";

export default function OverlayExample() {
  return (
    <AdaptiveBox viewBoxWidth={100} viewBoxHeight={100}>
      <AdaptiveBox.Svg>
        <circle cx="50" cy="50" r="40" fill="currentColor" />
      </AdaptiveBox.Svg>
      <div
        style={{
          position: "absolute",
          top: 0,
          left: 0,
          width: "100%",
          height: "100%",
          pointerEvents: "none",
          gridRow: "1 / 2", // Same row as SVG
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          zIndex: 10,
        }}
      >
        <span style={{ fontSize: "24px", fontWeight: "bold" }}>50%</span>
      </div>
      <AdaptiveBox.Label>With Overlay</AdaptiveBox.Label>
    </AdaptiveBox>
  );
}
```

## Performance Considerations

* **Pure CSS**: No JavaScript calculations for layout
* **Container queries**: Browser-optimized responsive sizing
* **CSS variables**: Computed once, cached by browser
* **No layout shifts**: Dimensions available from first render
* **Hardware acceleration**: CSS transforms and aspect-ratio are GPU-accelerated

## Integration with Components

All AudioUI components use AdaptiveBox internally. You typically don't need to use AdaptiveBox directly unless building custom controls. However, understanding AdaptiveBox helps when:

* Building custom controls with [Control Primitives](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/control-primitives)
* Creating custom layouts
* Understanding component sizing behavior
* Debugging layout issues

## Next Steps

* Learn about [Default Components Sizing](https://cutoff.dev/audio-ui/docs/latest/customization/default-components-sizing) for component sizing and base units
* Explore [Audio Parameters](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/audio-parameters) for parameter-based controls
* Check out [Control Primitives](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/control-primitives) for building custom controls
* See [View System and View Primitives](https://cutoff.dev/audio-ui/docs/latest/advanced-customization/view-system-view-primitives) for custom visualizations


---

# Audio Parameters

# Audio Parameters Model

> Learn how AudioUI's parameter model works, from value systems to scale functions and MIDI integration.

AudioUI uses a sophisticated parameter model that bridges the gap between user interface controls and audio processing. This model handles value conversion, quantization, scaling, and MIDI integration automatically.

## Why Parameters Matter

Audio applications need to handle three different value domains:

1. **Real values**: What your audio engine uses (e.g., 1000 Hz, -6 dB)
2. **Normalized values**: What the UI uses internally (0.0 to 1.0)
3. **MIDI values**: What external controllers send (0 to 127, or higher resolution)

The parameter model automatically converts between these domains, ensuring your UI always reflects the correct quantized state and handles MIDI input correctly.

## The Tripartite Value System

AudioUI uses a three-domain value system with MIDI as the pivot point:

### MIDI Value (The Source of Truth)

* **Type**: Integer
* **Range**: 0 to (2^Resolution - 1)
* **Characteristics**: Linear, discrete, quantized
* **Role**: Central pivot point for all conversions

### Normalized Value (UI Reality)

* **Type**: Float
* **Range**: 0.0 to 1.0
* **Characteristics**: Linear with respect to control travel and MIDI values
* **Usage**: Used by visual components and host automation

### Real Value (Audio Reality)

* **Type**: `number | boolean | string`
* **Range**: Defined by parameter type
* **Characteristics**: Can be logarithmic, exponential, discrete, or boolean
* **Usage**: What your application logic actually uses

## Parameter Types

AudioUI supports three parameter types:

### Continuous Parameters

Used for variable controls like volume, frequency, pan, etc.

```tsx title="ContinuousParameter.tsx" showLineNumbers
import { createContinuousParameter } from "@cutoff/audio-ui-react";
import { Knob } from "@cutoff/audio-ui-react";
import { useState } from "react";

const cutoffParam = createContinuousParameter({
  paramId: "cutoff",
  label: "Cutoff",
  min: 20,
  max: 20000,
  unit: "Hz",
  scale: "log",
  defaultValue: 1000,
});

export default function ContinuousParameterExample() {
  const [value, setValue] = useState(1000);

  return (
    <Knob
      parameter={cutoffParam}
      value={value}
      onChange={(e) => setValue(e.value)}
    />
  );
}
```

### Boolean Parameters

Used for on/off controls like mute, solo, bypass.

```tsx title="BooleanParameter.tsx" showLineNumbers
import { createBooleanParameter } from "@cutoff/audio-ui-react";
import { Button } from "@cutoff/audio-ui-react";
import { useState } from "react";

const powerParam = createBooleanParameter({
  paramId: "power",
  label: "Power",
  latch: true,
  defaultValue: false,
});

export default function BooleanParameterExample() {
  const [value, setValue] = useState(false);

  return (
    <Button
      parameter={powerParam}
      value={value}
      onChange={(e) => setValue(e.value)}
    />
  );
}
```

### Discrete Parameters

Used for selecting from a set of options like waveforms, filter types. The option definitions (value, label, optional midiValue) define the parameter structure; for how this relates to visual content (OptionView) in components, see [Options vs OptionView](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/options-and-optionview).

```tsx title="DiscreteParameter.tsx" showLineNumbers
import { createDiscreteParameter } from "@cutoff/audio-ui-react";
import { CycleButton } from "@cutoff/audio-ui-react";
import { useState } from "react";

const waveformParam = createDiscreteParameter({
  paramId: "waveform",
  label: "Waveform",
  options: [
    { value: "sine", label: "Sine" },
    { value: "square", label: "Square" },
    { value: "sawtooth", label: "Saw" },
    { value: "triangle", label: "Tri" },
  ],
  defaultValue: "sine",
});

export default function DiscreteParameterExample() {
  const [value, setValue] = useState("sine");

  return (
    <CycleButton
      parameter={waveformParam}
      value={value}
      onChange={(e) => setValue(e.value)}
    />
  );
}
```

## Creating Parameters

### Continuous Parameters

```tsx title="CreateContinuous.tsx" showLineNumbers
import { createContinuousParameter } from "@cutoff/audio-ui-react";

// Basic linear parameter
const volumeParam = createContinuousParameter({
  paramId: "volume",
  label: "Volume",
  min: 0,
  max: 100,
  unit: "%",
  defaultValue: 50,
});

// Logarithmic parameter (for frequency)
const frequencyParam = createContinuousParameter({
  paramId: "frequency",
  label: "Frequency",
  min: 20,
  max: 20000,
  unit: "Hz",
  scale: "log",
  defaultValue: 1000,
});

// Bipolar parameter (for pan)
const panParam = createContinuousParameter({
  paramId: "pan",
  label: "Pan",
  min: -100,
  max: 100,
  unit: "%",
  bipolar: true,
  defaultValue: 0,
});

// With step quantization
const attackParam = createContinuousParameter({
  paramId: "attack",
  label: "Attack",
  min: 0,
  max: 1000,
  unit: "ms",
  step: 1, // 1ms increments
  defaultValue: 0,
});
```

### Boolean Parameters

```tsx title="CreateBoolean.tsx" showLineNumbers
import { createBooleanParameter } from "@cutoff/audio-ui-react";

// Toggle (latch) parameter
const powerParam = createBooleanParameter({
  paramId: "power",
  label: "Power",
  latch: true,
  defaultValue: false,
});

// Momentary parameter
const recordParam = createBooleanParameter({
  paramId: "record",
  label: "Record",
  latch: false,
  defaultValue: false,
});
```

### Discrete Parameters

```tsx title="CreateDiscrete.tsx" showLineNumbers
import { createDiscreteParameter } from "@cutoff/audio-ui-react";

// Basic discrete parameter
const filterTypeParam = createDiscreteParameter({
  paramId: "filterType",
  label: "Filter Type",
  options: [
    { value: "lowpass", label: "Low Pass" },
    { value: "bandpass", label: "Band Pass" },
    { value: "highpass", label: "High Pass" },
  ],
  defaultValue: "lowpass",
});

// With MIDI values (structure is compatible with MIDI 2.0 Property Exchange valueSelect typeHint)
const waveformParam = createDiscreteParameter({
  paramId: "waveform",
  label: "Waveform",
  options: [
    { value: "sine", label: "Sine", midiValue: 0 },
    { value: "square", label: "Square", midiValue: 42 },
    { value: "sawtooth", label: "Saw", midiValue: 84 },
    { value: "triangle", label: "Tri", midiValue: 127 },
  ],
  defaultValue: "sine",
});
```

## Scale Functions

Scale functions transform how values are interpreted. They affect the control feel, not the step grid.

### Linear Scale (Default)

Standard linear mapping. Used for pan, modulation depth, etc.

```tsx title="LinearScale.tsx" showLineNumbers
const panParam = createContinuousParameter({
  paramId: "pan",
  label: "Pan",
  min: -100,
  max: 100,
  scale: "linear", // or omit (default)
  defaultValue: 0,
});
```

### Logarithmic Scale

Used for volume/dB, frequency (musical intervals).

```tsx title="LogScale.tsx" showLineNumbers
const volumeParam = createContinuousParameter({
  paramId: "volume",
  label: "Volume",
  min: -60,
  max: 6,
  unit: "dB",
  scale: "log",
  defaultValue: 0,
});

const frequencyParam = createContinuousParameter({
  paramId: "frequency",
  label: "Frequency",
  min: 20,
  max: 20000,
  unit: "Hz",
  scale: "log",
  defaultValue: 1000,
});
```

### Exponential Scale

Used for envelope curves, some filter parameters.

```tsx title="ExpScale.tsx" showLineNumbers
const attackParam = createContinuousParameter({
  paramId: "attack",
  label: "Attack",
  min: 0,
  max: 1000,
  unit: "ms",
  scale: "exp",
  defaultValue: 0,
});
```

## Bipolar Mode

Bipolar parameters are centered around zero. This affects both default values and visual rendering.

```tsx title="Bipolar.tsx" showLineNumbers
const panParam = createContinuousParameter({
  paramId: "pan",
  label: "Pan",
  min: -100,
  max: 100,
  bipolar: true, // Centers at zero
  defaultValue: 0, // Defaults to center (0.5 normalized)
});

// Visual rendering:
// - ValueRing starts at 12 o'clock (center)
// - ValueStrip fills from center
// - Controls show centered indicator
```

**Important**: The `bipolar` flag is independent of the numeric range. A parameter can be bipolar even if `min >= 0`.

## Step Quantization

The `step` parameter defines a linear grid in the real value domain, regardless of scale type.

### When Step Makes Sense

* **Linear scales**: Always works well
* **Log scales with linear units**: Works when the unit is linear (e.g., dB)
* **Exp scales with linear units**: Works when the unit is linear (e.g., milliseconds)

```tsx title="StepQuantization.tsx" showLineNumbers
// Volume with log scale: step makes sense (linear dB increments)
const volumeParam = createContinuousParameter({
  paramId: "volume",
  label: "Volume",
  min: -60,
  max: 6,
  step: 0.5, // 0.5 dB increments
  unit: "dB",
  scale: "log",
});

// Attack time with exp scale: step makes sense (linear ms increments)
const attackParam = createContinuousParameter({
  paramId: "attack",
  label: "Attack",
  min: 0,
  max: 1000,
  step: 1, // 1 ms increments
  unit: "ms",
  scale: "exp",
});
```

### When Step May Not Make Sense

For frequency (Hz) with log scale, linear Hz steps don't align with musical perception. Consider omitting `step` or using a very small value:

```tsx title="NoStep.tsx" showLineNumbers
// Frequency with log scale: step may not make sense
const freqParam = createContinuousParameter({
  paramId: "frequency",
  label: "Frequency",
  min: 20,
  max: 20000,
  // No step - allow smooth control across logarithmic range
  unit: "Hz",
  scale: "log",
});
```

## MIDI Resolution

MIDI resolution determines the quantization grid. Higher resolution provides finer control.

```tsx title="MidiResolution.tsx" showLineNumbers
// Standard 7-bit MIDI (0-127)
const standardParam = createContinuousParameter({
  paramId: "mod",
  label: "Mod Wheel",
  min: 0,
  max: 127,
  midiResolution: 7, // 7-bit (0-127)
  defaultValue: 0,
});

// High resolution (internal precision)
const preciseParam = createContinuousParameter({
  paramId: "cutoff",
  label: "Cutoff",
  min: 20,
  max: 20000,
  midiResolution: 32, // High resolution for internal precision
  defaultValue: 1000,
});
```

Available resolutions: `7`, `8`, `14`, `16`, `32`, `64`.

## Integration Examples

### Frequency Control

```tsx title="FrequencyControl.tsx" showLineNumbers
import { createContinuousParameter } from "@cutoff/audio-ui-react";
import { Knob, frequencyFormatter } from "@cutoff/audio-ui-react";
import { useState } from "react";

const cutoffParam = createContinuousParameter({
  paramId: "cutoff",
  label: "Cutoff",
  min: 20,
  max: 20000,
  unit: "Hz",
  scale: "log",
  defaultValue: 1000,
});

export default function FrequencyControlExample() {
  const [value, setValue] = useState(1000);

  return (
    <Knob
      parameter={cutoffParam}
      value={value}
      onChange={(e) => setValue(e.value)}
      valueFormatter={frequencyFormatter}
    />
  );
}
```

### Volume Control

```tsx title="VolumeControl.tsx" showLineNumbers
import { createContinuousParameter } from "@cutoff/audio-ui-react";
import { Slider } from "@cutoff/audio-ui-react";
import { useState } from "react";

const volumeParam = createContinuousParameter({
  paramId: "volume",
  label: "Volume",
  min: -60,
  max: 6,
  unit: "dB",
  scale: "log",
  step: 0.5,
  defaultValue: 0,
});

export default function VolumeControlExample() {
  const [value, setValue] = useState(0);

  return (
    <Slider
      parameter={volumeParam}
      value={value}
      onChange={(e) => setValue(e.value)}
      orientation="vertical"
    />
  );
}
```

### Waveform Selection

```tsx title="WaveformSelection.tsx" showLineNumbers
import { createDiscreteParameter } from "@cutoff/audio-ui-react";
import { CycleButton, OptionView } from "@cutoff/audio-ui-react";
import { useState } from "react";

const waveformParam = createDiscreteParameter({
  paramId: "waveform",
  label: "Waveform",
  options: [
    { value: "sine", label: "Sine" },
    { value: "square", label: "Square" },
    { value: "sawtooth", label: "Saw" },
    { value: "triangle", label: "Tri" },
  ],
  defaultValue: "sine",
});

export default function WaveformSelectionExample() {
  const [value, setValue] = useState("sine");

  return (
    <CycleButton
      parameter={waveformParam}
      value={value}
      onChange={(e) => setValue(e.value)}
    >
      <OptionView value="sine">Sine</OptionView>
      <OptionView value="square">Square</OptionView>
      <OptionView value="sawtooth">Saw</OptionView>
      <OptionView value="triangle">Tri</OptionView>
    </CycleButton>
  );
}
```

## Ad-Hoc vs Parameter Model

Components support two modes:

### Ad-Hoc Mode

Simple props for quick prototyping:

```tsx title="AdHocMode.tsx" showLineNumbers
<Knob
  value={value}
  onChange={(e) => setValue(e.value)}
  min={20}
  max={20000}
  label="Cutoff"
/>
```

### Parameter Model Mode

Full parameter definition for production use:

```tsx title="ParameterMode.tsx" showLineNumbers
const cutoffParam = createContinuousParameter({
  paramId: "cutoff",
  label: "Cutoff",
  min: 20,
  max: 20000,
  unit: "Hz",
  scale: "log",
  defaultValue: 1000,
});

<Knob
  parameter={cutoffParam}
  value={value}
  onChange={(e) => setValue(e.value)}
/>
```

**Benefits of Parameter Model**:

* Consistent value handling
* Automatic MIDI integration
* Proper quantization
* Scale function support
* Better integration with audio engines

## Component Compatibility

Not all components support all parameter types:

\| Component       | Supported Types | Notes                           |
\| :-------------- | :-------------- | :------------------------------ |
\| **Knob**        | `continuous`    | Variable control                |
\| **Slider**      | `continuous`    | Linear fader                    |
\| **Button**      | `boolean`       | Toggle or momentary             |
\| **CycleButton** | `discrete`      | Option selection                |

Components validate parameter types and will error if given an incompatible parameter.

## Best Practices

1. **Use parameter model for production**: Provides better integration and consistency
2. **Choose appropriate scales**: Log for frequency/volume, linear for pan/modulation
3. **Set step when it makes sense**: Use for integer values or meaningful increments
4. **Use bipolar for centered controls**: Pan, balance, etc.
5. **Set MIDI resolution appropriately**: Higher for internal precision, standard (7-bit) for MIDI CC
6. **Provide units**: Helps with formatting and user understanding

## Next Steps

* Read [Options vs OptionView](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/options-and-optionview) for the distinction between option definitions and OptionView visual content in discrete controls
* Learn about [Interaction System](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/interaction-system) for user input handling
* Explore [Control Primitives](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/control-primitives) for building custom parameter-based controls
* Check out [Default Components Styling](https://cutoff.dev/audio-ui/docs/latest/customization/default-components-styling) for theming default components
* See component documentation for parameter-specific features


---

# Options vs OptionView

# Options vs OptionView

> Understand the difference between the parameter model option definitions and the OptionView visual content in discrete controls.

Discrete controls in AudioUI (such as CycleButton, ImageRotarySwitch, and FilmStripDiscreteControl) can define their set of choices in two ways: through **option definitions** (the parameter model) and through **OptionView** children (the view). This page explains the distinction and when to use each.

## Model vs View

The library separates two concerns:

* **Option definitions (the model)**: Data that describes *which* values exist, their labels, and optional MIDI mapping. This drives value handling, cycling order, quantization, and MIDI integration. It does **not** define what is rendered (icons, styled text, custom components).
* **OptionView (the view)**: React elements that provide *what to render* for each option. Each OptionView has a `value` (matching an option) and `children` (any ReactNode). It does **not** define the parameter structure; it only supplies visual content.

You can use option definitions alone (the component will fall back to rendering labels), OptionView children alone (the component infers the option set from the children), or both together (definitions for the model, OptionView for custom visuals matched by value).

## When to Use OptionView

For basic use of discrete components, and especially when using **ad-hoc props** (no `parameter` prop), prefer **OptionView children**. In that case the option set is inferred from the OptionViews: each `<OptionView value="...">` contributes one option, and the same children are used for display. This is intuitive (similar to HTML `<select>` and `<option>`) and easy to extend (replace text with icons or custom components without touching the parameter model).

Use the `options` prop or a full `parameter` when you need explicit parameter structure: data-driven option lists, MIDI property mapping, or integration with the full [Audio Parameters](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/audio-parameters) model. See [CycleButton](https://cutoff.dev/audio-ui/docs/latest/components/vector/cycle-button) for the four modes (ad-hoc with children, options prop, strict parameter, hybrid).

## When to Use the options Prop or Parameter

Use **option definitions** (via the `options` prop or a discrete parameter from `createDiscreteParameter`) when:

* Options come from external or dynamic data (e.g. from an API, a parameter registry such as one built on WebMIDI, or a backend such as a Tauri command).
* You need explicit **midiValue** per option for MIDI CC or high-resolution mapping.
* You are building against the full parameter model and want a single source of truth for value handling, labels, and MIDI.

The discrete parameter model is designed to align with the **MIDI 2.0 Property Exchange** schema for controller descriptions. In particular, the structure of option definitions (value, label, optional midiValue) is compatible with the **valueSelect** typeHint used to describe controls that have a fixed set of selectable values. This allows the same option set to drive both the UI and PE-compliant controller metadata.

## Option Definitions Do Not Provide Visual Content

The `options` prop (and the `options` array inside a discrete parameter) defines **parameter structure** only. Each entry typically has:

* `value`: the real value (string or number)
* `label`: a string (used for fallback display or accessibility when no OptionView is provided)
* `midiValue` (optional): integer for MIDI mapping

These fields do **not** accept React nodes or custom visuals. To show icons, formatted text, or custom components for each option, use **OptionView** children and match them by `value`. When both are present, the component uses the option definitions for behavior and OptionView children for rendering (when a matching OptionView exists).

## OptionView Does Not Define the Parameter Model

OptionView components only supply **what to render** for a given value. They do not specify labels for automation, MIDI mapping, or the authoritative list of options. When you use only OptionView children (no `options` prop and no `parameter`), the component infers the list of options from the children. That is the recommended pattern for simple, ad-hoc discrete controls.

## Summary

\| Concern | Option definitions (`options` prop / parameter) | OptionView children |
\|--------|--------------------------------------------------|---------------------|
\| **Purpose** | Parameter structure (values, labels, MIDI) | Visual content (ReactNodes) |
\| **Defines** | Which options exist; cycling, quantization, MIDI | What is displayed per option |
\| **Preferred for** | Data-driven or parameter-model integration | Basic use; ad-hoc props; custom visuals |
\| **Inferred when omitted** | Yes, from OptionView children (when no `options`/`parameter`) | No; component falls back to option label |

For a full treatment of the parameter model, see [Audio Parameters](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/audio-parameters). For component-level details and modes, see [CycleButton](https://cutoff.dev/audio-ui/docs/latest/components/vector/cycle-button).


---

# Interaction System

# Interaction System

> Learn how AudioUI's unified interaction system handles user input across all control components, from drag and wheel to keyboard and touch.

AudioUI provides a unified interaction system that handles user input across all control components. The system is designed to be performant, accessible, and consistent with professional audio software standards.

## Overview

The interaction system supports multiple input methods:

* **Drag interactions**: Mouse and touch dragging with configurable sensitivity
* **Wheel interactions**: High-precision scrolling with accumulator support
* **Keyboard navigation**: Accessible control via arrow keys and specialized handlers
* **Pointer events**: Multi-touch support for polyphonic interactions (Keys component)

Each control type uses specialized interaction logic optimized for its use case:

* **Continuous controls** (Knob, Slider): Drag, wheel, and keyboard
* **Boolean controls** (Button): Click, drag-in/drag-out, keyboard
* **Discrete controls** (CycleButton): Click, keyboard stepping/cycling
* **Note-based controls** (Keys): Multi-touch, glissando detection

## Architecture

The interaction system consists of four main hooks, each wrapping a framework-agnostic controller:

### useContinuousInteraction

For continuous controls (Knob, Slider). Handles:

* **Pointer dragging**: Mouse and touch with adaptive sensitivity
* **Mouse wheel**: High-precision scrolling with configurable sensitivity
* **Keyboard navigation**: Arrow keys, Home/End, Space/Enter
* **Double-click reset**: Reset to default value
* **Focus management**: Consistent focus behavior

### useBooleanInteraction

For boolean controls (Button). Provides:

* **Momentary mode**: Press to activate, release to deactivate
* **Toggle mode**: Click to toggle state
* **Drag-in/drag-out**: Hardware-like behavior with global pointer tracking
* **Keyboard support**: Enter/Space for activation

### useDiscreteInteraction

For discrete/enum controls (CycleButton). Provides:

* **Cycling**: Click or Space/Enter to cycle to next value
* **Stepping**: Arrow keys to step up/down through options
* **Value resolution**: Automatically finds nearest valid option
* **Keyboard support**: Arrow keys for stepping, Space/Enter for cycling

### useNoteInteraction

For note-based controls (Keys). Provides:

* **Multi-touch support**: Tracks multiple concurrent pointers
* **Glissando detection**: Automatic note off/on when sliding across keys
* **Pointer capture**: Continues receiving events outside element
* **Touch action**: Prevents default touch behaviors

## Interaction Modes

### Drag Interactions

Drag interactions allow users to click/touch and drag to adjust values.

#### Direction

The drag direction determines how pointer movement maps to value changes:

* **Circular** (Knobs): Rotational drag around center
  * Clockwise = Increase
  * Counter-clockwise = Decrease
* **Vertical** (Vertical Sliders): Linear vertical drag
  * Up = Increase
  * Down = Decrease
* **Horizontal** (Horizontal Sliders): Linear horizontal drag
  * Right = Increase
  * Left = Decrease
* **Both**: Bidirectional drag (both axes affect value)

```tsx title="DragDirection.tsx" showLineNumbers
import { Knob, Slider } from "@cutoff/audio-ui-react";

export default function DragDirectionExample() {
  return (
    <div style={{ display: "flex", gap: "20px" }}>
      {/* Circular drag (default for knobs) */}
      <Knob
        value={0.5}
        onChange={(e) => {}}
        label="Circular"
        interactionDirection="circular"
      />
      {/* Vertical drag */}
      <Slider
        value={0.5}
        onChange={(e) => {}}
        label="Vertical"
        orientation="vertical"
        interactionDirection="vertical"
      />
      {/* Horizontal drag */}
      <Slider
        value={0.5}
        onChange={(e) => {}}
        label="Horizontal"
        orientation="horizontal"
        interactionDirection="horizontal"
      />
    </div>
  );
}
```

#### Sensitivity

Sensitivity controls how much the value changes per pixel of movement. Default is `0.005` (approximately 200px throw for full range).

```tsx title="Sensitivity.tsx" showLineNumbers
import { Knob } from "@cutoff/audio-ui-react";

export default function SensitivityExample() {
  return (
    <div style={{ display: "flex", gap: "20px" }}>
      {/* Low sensitivity - fine control */}
      <Knob
        value={0.5}
        onChange={(e) => {}}
        label="Fine"
        interactionSensitivity={0.001}
      />
      {/* Default sensitivity */}
      <Knob
        value={0.5}
        onChange={(e) => {}}
        label="Normal"
        interactionSensitivity={0.005}
      />
      {/* High sensitivity - coarse control */}
      <Knob
        value={0.5}
        onChange={(e) => {}}
        label="Coarse"
        interactionSensitivity={0.02}
      />
    </div>
  );
}
```

#### Adaptive Sensitivity

For low-resolution parameters (where step size is large), sensitivity is automatically adjusted to ensure a single step change requires no more than 30 pixels of movement. This prevents "dead zones" in discrete-like continuous controls.

The adaptive calculation uses:

```
effectiveSensitivity = Math.max(baseSensitivity, stepSize / 30)
```

#### Drag Accumulator

For stepped parameters, small drag movements accumulate until they exceed the step size. This prevents unresponsive behavior when dragging slowly on low-resolution parameters.

### Wheel Interactions

Wheel interactions provide high-precision scrolling for fine adjustments.

#### Behavior

* **Direction**: Standard scrolling (Up/Push = Increase, Down/Pull = Decrease)
* **Page scroll**: Strictly prevented - wheel events stop propagation to avoid scrolling the page
* **Sensitivity**: Configurable, defaults to `0.005`
* **Decoupled**: Wheel sensitivity is independent from drag sensitivity

#### Wheel Accumulator

For stepped parameters, wheel deltas are accumulated until they exceed the step size. This ensures reliable operation across different hardware (trackpads with small deltas vs. mice with large clicks) and prevents landing "between" steps.

```tsx title="WheelInteraction.tsx" showLineNumbers
import { Knob } from "@cutoff/audio-ui-react";

export default function WheelInteractionExample() {
  return (
    <div style={{ display: "flex", gap: "20px" }}>
      {/* Wheel only */}
      <Knob
        value={0.5}
        onChange={(e) => {}}
        label="Wheel Only"
        interactionMode="wheel"
      />
      {/* Both drag and wheel */}
      <Knob
        value={0.5}
        onChange={(e) => {}}
        label="Drag & Wheel"
        interactionMode="both"
      />
    </div>
  );
}
```

### Keyboard Interactions

Keyboard interactions provide accessible control and fine-grained adjustments.

#### Focus Management

Controls are focusable via Tab navigation. Focused controls display a high-contrast highlight effect (brightness/contrast boost + shadow) instead of a browser-default focus ring.

#### Keyboard Shortcuts

**Continuous Controls** (Knob, Slider):

* `Arrow Up` / `Arrow Right`: Increment value
* `Arrow Down` / `Arrow Left`: Decrement value
* `Home`: Set to minimum
* `End`: Set to maximum
* `Space` / `Enter`: Specialized handlers (component-specific)

**Discrete Controls** (CycleButton):

* `Arrow Up` / `Arrow Right`: Step to next value (clamped at max)
* `Arrow Down` / `Arrow Left`: Step to previous value (clamped at min)
* `Space` / `Enter`: Cycle to next value (wraps around)

**Boolean Controls** (Button):

* `Space` / `Enter`: Toggle/Activate

```tsx title="KeyboardInteraction.tsx" showLineNumbers
import { Knob, CycleButton } from "@cutoff/audio-ui-react";

export default function KeyboardInteractionExample() {
  return (
    <div style={{ display: "flex", gap: "20px" }}>
      {/* Focus and use arrow keys */}
      <Knob
        value={0.5}
        onChange={(e) => {}}
        label="Focus & Use Arrows"
      />
      {/* CycleButton with keyboard stepping */}
      <CycleButton
        value="option1"
        options={[
          { value: "option1", label: "Option 1" },
          { value: "option2", label: "Option 2" },
          { value: "option3", label: "Option 3" },
        ]}
        onChange={(e) => {}}
        label="Use Arrows to Step"
      />
    </div>
  );
}
```

### Pointer Events (Keys)

The Keys component uses pointer events for multi-touch and glissando support.

#### Multi-Touch

Supports multiple concurrent touches for polyphonic playing. Each touch is tracked independently.

#### Glissando Detection

Sliding across keys automatically triggers:

* Note off for the previous key
* Note on for the new key

This enables smooth glissando effects when sliding across the keyboard.

#### Pointer Capture

Uses pointer capture to continue receiving events even when the pointer leaves the element, ensuring reliable glissando detection across the entire keyboard.

## Interaction Mode Configuration

### interactionMode

Controls which input methods are enabled:

* **`"drag"`**: Only drag interactions enabled
* **`"wheel"`**: Only wheel interactions enabled
* **`"both"`**: Both drag and wheel enabled (default)

```tsx title="InteractionMode.tsx" showLineNumbers
import { Knob } from "@cutoff/audio-ui-react";

export default function InteractionModeExample() {
  return (
    <div style={{ display: "flex", gap: "20px" }}>
      <Knob
        value={0.5}
        onChange={(e) => {}}
        label="Drag Only"
        interactionMode="drag"
      />
      <Knob
        value={0.5}
        onChange={(e) => {}}
        label="Wheel Only"
        interactionMode="wheel"
      />
      <Knob
        value={0.5}
        onChange={(e) => {}}
        label="Both"
        interactionMode="both"
      />
    </div>
  );
}
```

## Component-Specific Behavior

### Knob & Slider (Continuous)

* **Drag**: Standard continuous adjustment with circular (knobs) or linear (sliders) direction
* **Wheel**: Fine adjustments with accumulator support for stepped parameters
* **Keyboard**: Step-based increments with configurable step size
* **Double-click**: Reset to default value (when `resetToDefault` is provided)

### Button (Boolean)

* **Click**: Toggle or momentary activation
* **Drag-in/drag-out**: Hardware-like behavior where buttons respond to pointer entering/leaving while pressed
* **Global pointer tracking**: Tracks pointer state globally to enable drag-in behavior from anywhere on the page
* **Step sequencer pattern**: Enables classic step sequencer interactions where multiple buttons can be activated with a single drag gesture

```tsx title="ButtonDragIn.tsx" showLineNumbers
import { Button } from "@cutoff/audio-ui-react";

export default function ButtonDragInExample() {
  return (
    <div style={{ display: "flex", gap: "10px" }}>
      {/* Drag across buttons to activate them */}
      {[0, 1, 2, 3, 4, 5, 6, 7].map((step) => (
        <Button
          key={step}
          value={false}
          onChange={(e) => {}}
          label={`Step ${step + 1}`}
          latch={true}
        />
      ))}
    </div>
  );
}
```

### CycleButton (Discrete)

* **Click**: Cycles to next value (wraps around to start)
* **Keyboard stepping**: Arrow keys step through options (clamped at min/max)
* **Keyboard cycling**: Space/Enter cycles to next value (wraps around)
* **No drag**: Discrete-only control - does not support continuous interaction

### Keys (Note-Based)

* **Multi-touch**: Multiple concurrent touches for polyphonic playing
* **Glissando**: Sliding across keys automatically triggers note off/on
* **Pointer capture**: Continues receiving events outside element
* **Touch action**: Prevents default touch behaviors (scrolling, zooming)

## Sensitivity Tuning

### Drag Sensitivity

Controls how much the value changes per pixel of movement:

* **Default**: `0.005` (approximately 200px throw for full range)
* **Fine control**: Lower values (e.g., `0.001`)
* **Coarse control**: Higher values (e.g., `0.02`)

### Wheel Sensitivity

Separate from drag sensitivity to allow independent tuning:

* **Default**: `0.005`
* **Decoupled**: Prevents "too fast" scrolling on low-resolution parameters that use boosted drag sensitivity

### Adaptive Behavior

For stepped parameters, sensitivity is automatically adjusted to ensure responsiveness:

```
effectiveSensitivity = Math.max(baseSensitivity, stepSize / 30)
```

This ensures that a single step change requires no more than 30 pixels of movement.

## Accessibility

### ARIA Attributes

Components automatically receive appropriate ARIA attributes:

* **Continuous controls**: `role="slider"`, `aria-valuenow`, `aria-valuemin`, `aria-valuemax`, `aria-label`
* **Boolean controls**: `role="button"`, `aria-pressed`, `aria-label`
* **Discrete controls**: `role="button"`, `aria-label`

### Focus Management

* **Tab navigation**: All controls are focusable
* **Visual feedback**: High-contrast highlight effect on focus (replaces default outline)
* **Keyboard shortcuts**: Full keyboard support for all interaction types

### Text Selection Prevention

Text selection is automatically disabled (`user-select: none`) during drag operations to prevent accidental selection.

## Cursor Behavior

The interaction system provides intelligent cursor feedback based on the control's interaction capabilities and state.

### Continuous Controls

Cursor depends on interaction configuration:

* **Disabled**: `not-allowed`
* **Wheel only**: `ns-resize` (vertical)
* **Horizontal drag**: `ew-resize` (horizontal)
* **Vertical drag**: `ns-resize` (vertical)
* **Bidirectional drag**: `move` (bidirectional)
* **Circular drag**: Custom circular cursor
* **Clickable only**: `pointer`

### Boolean & Discrete Controls

* **Interactive**: `pointer`
* **Non-interactive**: Default browser cursor

### Cursor Customization

All cursor values are customizable via CSS variables:

* `--audioui-cursor-clickable`: Clickable controls (default: `pointer`)
* `--audioui-cursor-bidirectional`: Bidirectional drag (default: SVG data URI)
* `--audioui-cursor-horizontal`: Horizontal drag (default: SVG data URI)
* `--audioui-cursor-vertical`: Vertical drag (default: SVG data URI)
* `--audioui-cursor-circular`: Circular drag (custom circular cursor)
* `--audioui-cursor-noneditable`: Non-editable controls (default: `default`)
* `--audioui-cursor-disabled`: Disabled controls (default: `not-allowed`)

**Note**: The library uses SVG data URIs for bidirectional, horizontal, and vertical cursors to ensure consistent behavior across browsers.

## Performance Considerations

### Lazy Listeners

Global `mousemove`/`touchmove` listeners are only attached during an active drag session and removed immediately after.

### Refs for State

Mutable state (drag start position, current value) is tracked in `useRef` to avoid stale closures and unnecessary re-renders during high-frequency events.

### Native Events

Wheel handling uses native non-passive listeners (via `AdaptiveBox`) to reliably prevent page scrolling, which React's synthetic events cannot always guarantee.

### Accumulators

For stepped parameters, drag and wheel accumulators prevent unnecessary value updates and ensure smooth interaction even with slow movements.

## Integration with Control Primitives

The interaction system is fully integrated with [Control Primitives](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/control-primitives):

* **ContinuousControl**: Uses `useContinuousInteraction`
* **DiscreteControl**: Uses `useDiscreteInteraction`
* **BooleanControl**: Uses `useBooleanInteraction`

When building custom controls, the interaction system is automatically available through the primitives.

## Next Steps

* Learn about [Audio Parameters](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/audio-parameters) for parameter-based value management
* Explore [Control Primitives](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/control-primitives) for building custom controls with full interaction support
* Check out component-specific documentation for interaction examples


---

# Control Primitives and Views

# Control Primitives and Views

> Introduction to AudioUI's control primitives and view system - the building blocks for creating custom controls with full library features.

Control primitives are low-level components that separate behavior from visualization. They provide all the interaction logic, parameter handling, and accessibility features while letting you define the visual appearance.

## What Are Control Primitives?

Control primitives are generic components that handle:

* **Interaction logic**: Drag, wheel, keyboard input (see [Interaction System](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/interaction-system))
* **Parameter management**: Value conversion, quantization, MIDI
* **Accessibility**: ARIA attributes, focus management, keyboard navigation
* **Layout system**: AdaptiveBox integration, sizing, labels

You provide the **visualization** (the view component), and the primitive handles everything else.

## Architecture: Behavior vs Visualization

The key insight is separation of concerns:

```
Control Primitive (Behavior)
    ↓
View Component (Visualization)
    ↓
SVG Primitives (Graphics)
```

This architecture allows you to:

* Use any visualization you want (SVG, bitmap, custom)
* Reuse interaction logic across different visual styles
* Build custom controls without reimplementing complex behavior

## The Three Primitives

AudioUI provides three control primitives:

### ContinuousControl

For continuous value controls (knobs, sliders, faders).

```tsx title="ContinuousControlBasic.tsx" showLineNumbers
import { ContinuousControl } from "@cutoff/audio-ui-react";
import { useState } from "react";

// Simple view component
function SimpleKnobView({ normalizedValue }: { normalizedValue: number }) {
  const angle = normalizedValue * 270 - 135; // 270 degree arc
  return (
    <g>
      <circle cx="50" cy="50" r="45" fill="none" stroke="#ccc" strokeWidth="4" />
      <line
        x1="50"
        y1="50"
        x2={50 + 40 * Math.cos((angle * Math.PI) / 180)}
        y2={50 + 40 * Math.sin((angle * Math.PI) / 180)}
        stroke="currentColor"
        strokeWidth="3"
      />
    </g>
  );
}

export default function ContinuousControlBasicExample() {
  const [value, setValue] = useState(0.5);

  return (
    <ContinuousControl
      view={SimpleKnobView}
      viewBoxWidth={100}
      viewBoxHeight={100}
      value={value}
      onChange={(e) => setValue(e.value)}
      min={0}
      max={1}
      label="Custom Knob"
    />
  );
}
```

### DiscreteControl

For discrete/enum value controls (option selectors, rotary switches).

```tsx title="DiscreteControlBasic.tsx" showLineNumbers
import { DiscreteControl, OptionView } from "@cutoff/audio-ui-react";
import { useState } from "react";

function SimpleSelectorView({ normalizedValue }: { normalizedValue: number }) {
  const positions = 4;
  const currentPosition = Math.round(normalizedValue * (positions - 1));
  const angle = (currentPosition / positions) * 360;

  return (
    <g>
      <circle cx="50" cy="50" r="45" fill="none" stroke="#ccc" strokeWidth="4" />
      <line
        x1="50"
        y1="50"
        x2={50 + 35 * Math.cos((angle * Math.PI) / 180)}
        y2={50 + 35 * Math.sin((angle * Math.PI) / 180)}
        stroke="currentColor"
        strokeWidth="4"
      />
    </g>
  );
}

export default function DiscreteControlBasicExample() {
  const [value, setValue] = useState("option1");

  return (
    <DiscreteControl
      view={SimpleSelectorView}
      viewBoxWidth={100}
      viewBoxHeight={100}
      value={value}
      onChange={(e) => setValue(e.value)}
      label="Selector"
    >
      <OptionView value="option1">1</OptionView>
      <OptionView value="option2">2</OptionView>
      <OptionView value="option3">3</OptionView>
      <OptionView value="option4">4</OptionView>
    </DiscreteControl>
  );
}
```

### BooleanControl

For boolean (on/off) controls (switches, buttons).

```tsx title="BooleanControlBasic.tsx" showLineNumbers
import { BooleanControl } from "@cutoff/audio-ui-react";
import { useState } from "react";

function SimpleSwitchView({ normalizedValue }: { normalizedValue: number }) {
  const isOn = normalizedValue > 0.5;

  return (
    <g>
      <rect
        x="20"
        y="40"
        width="60"
        height="20"
        rx="10"
        fill={isOn ? "currentColor" : "#ccc"}
      />
      <circle
        cx={isOn ? 70 : 30}
        cy="50"
        r="8"
        fill="white"
      />
    </g>
  );
}

export default function BooleanControlBasicExample() {
  const [value, setValue] = useState(false);

  return (
    <BooleanControl
      view={SimpleSwitchView}
      viewBoxWidth={100}
      viewBoxHeight={100}
      value={value}
      onChange={(e) => setValue(e.value)}
      latch={true}
      label="Power"
    />
  );
}
```

## When to Use Primitives

### Use Built-in Components When:

* You need standard audio UI controls (Knob, Slider, Button, CycleButton)
* You want multiple visual variants out of the box
* You need theming support (color, roundness); use the [vector components](https://cutoff.dev/audio-ui/docs/latest/components/vector/introduction) instead; see [Theming Features](https://cutoff.dev/audio-ui/docs/latest/components/vector/introduction#theming-features) for built-in color and roundness
* You're building a standard audio interface

### Use Primitives When:

* You need a completely custom visual design
* You want to match existing plugin aesthetics
* You're building bitmap-based controls (filmstrips, images)
* You need specialized interaction patterns
* You're creating a unique control type

## The View Component Contract

View components must implement the `ControlComponentView` interface:

```typescript
interface ControlComponentView {
  viewBox: { width: number; height: number };
  labelHeightUnits?: number; // Default: 20
  interaction?: {
    mode: "drag" | "wheel" | "both";
    direction: "vertical" | "horizontal" | "circular" | "both";
  };
}
```

### Static Properties

Define these as static properties on your view component:

```tsx title="ViewComponentContract.tsx" showLineNumbers
function MyCustomView({ normalizedValue }: ControlComponentViewProps) {
  // Your visualization code
  return <g>{/* SVG content */}</g>;
}

// Required static properties
MyCustomView.viewBox = { width: 100, height: 100 };
MyCustomView.labelHeightUnits = 15; // Optional, defaults to 20
MyCustomView.interaction = {
  mode: "both",
  direction: "circular",
}; // Optional, provides defaults
```

### View Props

Your view component receives these props:

```typescript
type ControlComponentViewProps = {
  normalizedValue: number; // 0.0 to 1.0
  children?: React.ReactNode; // For center content (HTML overlay)
  className?: string;
  style?: React.CSSProperties;
  // ... any custom props you define
};
```

## Simple Custom Control Example

Here's a complete example of a custom knob:

```tsx title="CustomKnob.tsx" showLineNumbers
import { ContinuousControl, ControlComponentViewProps } from "@cutoff/audio-ui-react";
import { useState } from "react";

function CustomKnobView({ normalizedValue }: ControlComponentViewProps) {
  // Calculate angle (270 degree arc, starting at -135 degrees)
  const angle = normalizedValue * 270 - 135;
  const radians = (angle * Math.PI) / 180;

  return (
    <g>
      {/* Background arc */}
      <path
        d={`M ${50 + 45 * Math.cos((-135 * Math.PI) / 180)} ${
          50 + 45 * Math.sin((-135 * Math.PI) / 180)
        } A 45 45 0 1 1 ${50 + 45 * Math.cos((135 * Math.PI) / 180)} ${
          50 + 45 * Math.sin((135 * Math.PI) / 180)
        }`}
        fill="none"
        stroke="rgba(0,0,0,0.2)"
        strokeWidth="4"
      />
      {/* Value arc */}
      <path
        d={`M ${50 + 45 * Math.cos((-135 * Math.PI) / 180)} ${
          50 + 45 * Math.sin((-135 * Math.PI) / 180)
        } A 45 45 0 ${normalizedValue > 0.5 ? 1 : 0} 1 ${
          50 + 45 * Math.cos(radians)
        } ${50 + 45 * Math.sin(radians)}`}
        fill="none"
        stroke="currentColor"
        strokeWidth="4"
        strokeLinecap="round"
      />
      {/* Indicator line */}
      <line
        x1="50"
        y1="50"
        x2={50 + 40 * Math.cos(radians)}
        y2={50 + 40 * Math.sin(radians)}
        stroke="currentColor"
        strokeWidth="3"
        strokeLinecap="round"
      />
    </g>
  );
}

// Define static properties
CustomKnobView.viewBox = { width: 100, height: 100 };
CustomKnobView.labelHeightUnits = 15;
CustomKnobView.interaction = {
  mode: "both",
  direction: "circular",
};

export default function CustomKnobExample() {
  const [value, setValue] = useState(0.5);

  return (
    <ContinuousControl
      view={CustomKnobView}
      value={value}
      onChange={(e) => setValue(e.value)}
      min={0}
      max={1}
      label="Custom Knob"
    />
  );
}
```

## What You Get for Free

When you use control primitives, you automatically get:

* **Drag interaction**: Mouse and touch support
* **Wheel interaction**: Scroll to adjust values
* **Keyboard support**: Arrow keys, Home/End
* **Focus management**: Proper focus handling and visual feedback
* **Accessibility**: ARIA attributes and keyboard navigation
* **Parameter integration**: Full [Audio Parameter](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/audio-parameters) support
* **Layout system**: [AdaptiveBox](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/adaptive-box-layout) integration
* **Sizing**: Fixed and adaptive sizing support
* **Labels**: Automatic label positioning and alignment

## Next Steps

For detailed information on each primitive:

* [ContinuousControl Primitive](https://cutoff.dev/audio-ui/docs/latest/advanced-customization/continuous-control-primitive) - Complete API reference
* [DiscreteControl Primitive](https://cutoff.dev/audio-ui/docs/latest/advanced-customization/discrete-control-primitive) - Options, modes, and advanced usage
* [BooleanControl Primitive](https://cutoff.dev/audio-ui/docs/latest/advanced-customization/boolean-control-primitive) - Latch, momentary, and drag-in/out

For building custom visualizations:

* [View System and View Primitives](https://cutoff.dev/audio-ui/docs/latest/advanced-customization/view-system-view-primitives) - View building blocks for custom controls


---

# View System and View Primitives

# View System and View Primitives

> Learn how to compose custom controls using AudioUI's view primitives—the building blocks for creating unique visual designs (including vector primitives for SVG).

Vector primitives are low-level building blocks for composing custom radial and linear controls. They provide a consistent API for positioning content, rendering value indicators, and creating scale decorations.

These view primitives, together with the [Control Primitives](https://cutoff.dev/audio-ui/docs/latest/advanced-topics/control-primitives), allow you to create entirely new components, possibly independent of the theming and size system of the built-in components (built-in theming is described in [Theming Features](https://cutoff.dev/audio-ui/docs/latest/components/vector/introduction#theming-features)), like this:

Creating the view represents 99% of the work when building a new custom component in AudioUI, because the Control Primitives handle layout, the parameter model (conversions, units, scaling, …), user interactions, and more.

## Overview

AudioUI provides three categories of vector primitives:

* **Radial Primitives**: For rotary controls (knobs, dials, encoders)
* **Linear Primitives**: For linear controls (sliders, faders, pitch/mod wheels)
* **Other Primitives**: Rectangular or path-based building blocks (images, filmstrips, path reveal, HTML overlay)

All primitives share a common coordinate system and work together to create cohesive control designs.

## Radial Primitives

Radial primitives use a center-point coordinate system: `cx`, `cy` (center coordinates) and `radius` (distance from center). In this system, views are composed of several radial primitives that share a common center point and use increasing radius values.

![Radial primitives diagram](https://cutoff.dev/images/documentation/svg-primitives-radial-light@0.7x.png)

### ValueRing

Renders a circular arc indicator showing value progress.

```tsx title="ValueRingBasic.tsx" showLineNumbers
import { ValueRing } from "@cutoff/audio-ui-react";

function CustomKnobView({ normalizedValue }: ControlComponentViewProps) {
  return (
    <g>
      <ValueRing
        cx={50}
        cy={50}
        radius={45}
        normalizedValue={normalizedValue}
        thickness={4}
        openness={90}
        fgArcStyle={{ stroke: "currentColor" }}
        bgArcStyle={{ stroke: "rgba(0,0,0,0.2)" }}
      />
    </g>
  );
}
```

**Key Features**:

* Bipolar mode support (starts from center)
* Configurable thickness, roundness, openness
* Background and foreground arc styling

### RotaryImage

Rotates children based on normalized value. Shares angle logic with ValueRing.

```tsx title="RotaryImageBasic.tsx" showLineNumbers
import { RotaryImage } from "@cutoff/audio-ui-react";

function CustomKnobView({ normalizedValue }: ControlComponentViewProps) {
  return (
    <g>
      <RotaryImage
        cx={50}
        cy={50}
        radius={35}
        normalizedValue={normalizedValue}
        openness={90}
      >
        <line
          x1="50%"
          y1="15%"
          x2="50%"
          y2="5%"
          stroke="currentColor"
          strokeWidth={3}
        />
      </RotaryImage>
    </g>
  );
}
```

### RadialImage

Displays static content (image or SVG) at radial coordinates.

```tsx title="RadialImageBasic.tsx" showLineNumbers
import { RadialImage } from "@cutoff/audio-ui-react";

function CustomKnobView({ normalizedValue }: ControlComponentViewProps) {
  return (
    <g>
      {/* Static background texture */}
      <RadialImage
        cx={50}
        cy={50}
        radius={40}
        imageHref="/knob-texture.png"
      />
      {/* Rotating indicator */}
      <RotaryImage
        cx={50}
        cy={50}
        radius={35}
        normalizedValue={normalizedValue}
      >
        <line x1="50%" y1="10%" x2="50%" y2="0%" stroke="white" strokeWidth={2} />
      </RotaryImage>
    </g>
  );
}
```

### TickRing

Renders a ring of tick marks for scale decoration.

```tsx title="TickRingBasic.tsx" showLineNumbers
import { TickRing } from "@cutoff/audio-ui-react";

function CustomKnobView({ normalizedValue }: ControlComponentViewProps) {
  return (
    <g>
      <TickRing
        cx={50}
        cy={50}
        radius={45}
        thickness={3}
        count={11}
        openness={90}
        style={{ stroke: "currentColor", strokeWidth: 1 }}
      />
      <ValueRing
        cx={50}
        cy={50}
        radius={45}
        normalizedValue={normalizedValue}
        thickness={4}
      />
    </g>
  );
}
```

### LabelRing

Renders text or icons at radial positions (wrapper around TickRing).

```tsx title="LabelRingBasic.tsx" showLineNumbers
import { LabelRing } from "@cutoff/audio-ui-react";

function CustomKnobView({ normalizedValue }: ControlComponentViewProps) {
  return (
    <g>
      <LabelRing
        cx={50}
        cy={50}
        radius={45}
        labels={[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}
        labelClassName="text-xs font-medium"
      />
      <ValueRing
        cx={50}
        cy={50}
        radius={45}
        normalizedValue={normalizedValue}
        thickness={4}
      />
    </g>
  );
}
```

## Linear Primitives

Linear primitives use a strip model: `cx`, `cy` (center point), `length`, and `rotation`. In this system, views are composed of several linear primitives that share a common `length` and `cy` value but may use different `cx` values.

For example, the central strip of a fader (at the center point) may be surrounded by parallel thick strips and label strips to form the scale and markers of the fader.

The center/length model is used rather than a perhaps more intuitive Cartesian system because it fits components that can have different orientations (e.g. horizontal or vertical sliders).

Typically, linear views are created in a vertical layout; their orientation is then set in the control primitive via the `rotation` attribute (e.g. 270° or -90° for a horizontal slider that grows from left to right).

![Linear primitives diagram](https://cutoff.dev/images/documentation/svg-primitives-linear-light@0.7x.png)

### LinearStrip

Renders a rectangular strip (background track).

```tsx title="LinearStripBasic.tsx" showLineNumbers
import { LinearStrip, ValueStrip, LinearCursor } from "@cutoff/audio-ui-react";

function CustomSliderView({ normalizedValue }: ControlComponentViewProps) {
  return (
    <g>
      {/* Background track */}
      <LinearStrip
        cx={50}
        cy={150}
        length={260}
        thickness={6}
        roundness={0.3}
      />
      {/* Value fill */}
      <ValueStrip
        cx={50}
        cy={150}
        length={260}
        thickness={6}
        normalizedValue={normalizedValue}
        roundness={0.3}
      />
      {/* Cursor */}
      <LinearCursor
        cx={50}
        cy={150}
        length={260}
        normalizedValue={normalizedValue}
        width={8}
        aspectRatio={1}
        roundness={1}
      />
    </g>
  );
}
```

### ValueStrip

Renders the active (foreground) portion of a linear strip.

```tsx title="ValueStripBasic.tsx" showLineNumbers
import { LinearStrip, ValueStrip } from "@cutoff/audio-ui-react";

function CustomFaderView({ normalizedValue }: ControlComponentViewProps) {
  return (
    <g>
      <LinearStrip cx={50} cy={150} length={260} thickness={8} />
      <ValueStrip
        cx={50}
        cy={150}
        length={260}
        thickness={8}
        normalizedValue={normalizedValue}
        bipolar={false} // Unipolar: fills from bottom
      />
    </g>
  );
}
```

**Bipolar Mode**:

```tsx title="ValueStripBipolar.tsx" showLineNumbers
<ValueStrip
  cx={50}
  cy={150}
  length={260}
  thickness={8}
  normalizedValue={normalizedValue}
  bipolar={true} // Fills from center
/>
```

### LinearCursor

Renders a cursor that slides along a linear strip.

```tsx title="LinearCursorBasic.tsx" showLineNumbers
import { LinearCursor } from "@cutoff/audio-ui-react";

function CustomSliderView({ normalizedValue }: ControlComponentViewProps) {
  return (
    <LinearCursor
      cx={50}
      cy={150}
      length={260}
      normalizedValue={normalizedValue}
      width={10}
      aspectRatio={1.5} // Height = width / 1.5
      roundness={0.5}
    />
  );
}
```

**Image-based cursor**:

```tsx title="LinearCursorImage.tsx" showLineNumbers
<LinearCursor
  cx={50}
  cy={150}
  length={260}
  normalizedValue={normalizedValue}
  width={20}
  imageHref="/cursor-handle.png" // Preserves natural aspect ratio
/>
```

## Other Primitives

These primitives are neither radial nor linear. They use rectangular coordinates or arbitrary paths and are useful for static images, value-driven path reveals, filmstrip frames, and HTML overlays.

### Image

Displays static content (image or SVG) at rectangular coordinates. Position is defined by top-left corner `(x, y)` and `width` / `height`.

```tsx title="ImageBasic.tsx" showLineNumbers
import { Image } from "@cutoff/audio-ui-react";

function CustomView() {
  return (
    <g>
      <Image
        x={10}
        y={10}
        width={80}
        height={40}
        imageHref="/icon.png"
      />
      {/* Or SVG content via children */}
      <Image x={10} y={60} width={24} height={24}>
        <path d="M12 2L2 7v10l10 5 10-5V7L12 2z" fill="currentColor" />
      </Image>
    </g>
  );
}
```

**Key Features**:

* Optional `imageHref` for bitmap images or `children` for SVG (e.g. icons)
* Optional `transform` for SVG transform attribute
* `style.color` supports icon theming when using `currentColor`

### RevealingPath

Reveals an SVG path from start to end based on a normalized value. Uses `pathLength` and stroke-dashoffset for performant fill animations.

```tsx title="RevealingPathBasic.tsx" showLineNumbers
import { RevealingPath } from "@cutoff/audio-ui-react";

function CustomView({ normalizedValue }: ControlComponentViewProps) {
  return (
    <g>
      <RevealingPath
        d="M 10 50 Q 50 10 90 50 T 170 50"
        normalizedValue={normalizedValue}
        stroke="currentColor"
        strokeWidth={4}
        fill="none"
      />
    </g>
  );
}
```

**Key Features**:

* `normalizedValue` 0 = hidden, 1 = fully visible
* Optional `resolution` for path length calculation (default 100)
* Accepts standard SVG path props (`stroke`, `fill`, `strokeWidth`, etc.)

### FilmstripImage

Displays a single frame from a sprite sheet (filmstrip) based on a normalized value. Frames are stacked vertically or horizontally; the component uses a shifted viewBox for hardware-accelerated scrubbing.

```tsx title="FilmstripImageBasic.tsx" showLineNumbers
import { FilmstripImage } from "@cutoff/audio-ui-react";

function CustomView({ normalizedValue }: ControlComponentViewProps) {
  return (
    <g>
      <FilmstripImage
        x={0}
        y={0}
        frameWidth={100}
        frameHeight={100}
        frameCount={64}
        normalizedValue={normalizedValue}
        imageHref="/knob-filmstrip.png"
        orientation="vertical"
      />
    </g>
  );
}
```

**Key Features**:

* `frameWidth`, `frameHeight`, `frameCount` define the strip layout
* `orientation`: `"vertical"` (default) or `"horizontal"`
* Optional `frameRotation` (degrees), `invertValue` for reversed mapping

### RadialHtmlOverlay

Renders HTML content inside the SVG at a radial position. It creates a square `foreignObject` centered at `(cx, cy)` with side length `radius * 2`. Use it for text or rich content in knobs when you need HTML layout (e.g. Flexbox, fonts).

```tsx title="RadialHtmlOverlayBasic.tsx" showLineNumbers
import { RadialHtmlOverlay } from "@cutoff/audio-ui-react";

function CustomKnobView({ normalizedValue }: ControlComponentViewProps) {
  return (
    <g>
      {/* SVG layers (ValueRing, etc.) */}
      <RadialHtmlOverlay cx={50} cy={50} radius={30} pointerEvents="none">
        <div style={{ fontSize: 14, fontWeight: 700, textAlign: "center" }}>
          {Math.round(normalizedValue * 100)}%
        </div>
      </RadialHtmlOverlay>
    </g>
  );
}
```

**Key Features**:

* `pointerEvents`: `"none"` (default) lets interactions pass through to the control; use `"auto"` if the overlay is interactive
* **Safari**: For complex layouts, the docs recommend rendering center content as `children` of the control primitive (HTML overlay outside SVG) instead of `RadialHtmlOverlay` for better compatibility. See [Center Content](#center-content) below.

## Composing Custom Knobs

This section walks through building the **Hi-Fi Knob** (the demo above) step by step. You combine vector primitives with [ContinuousControl](https://cutoff.dev/audio-ui/docs/latest/advanced-customization/continuous-control-primitive) to get a fully interactive knob that is independent of the built-in theming and sizing.

### Step 1: Scale Decoration

Start with the scale: two TickRings give a hi-fi style dial—a subtle inner ring and a bolder outer ring.

```tsx title="HifiKnobStep1.tsx" showLineNumbers
import { TickRing } from "@cutoff/audio-ui-react";

function HifiKnobView({ normalizedValue }: ControlComponentViewProps) {
  return (
    <g>
      <TickRing
        cx={50} cy={50} radius={50} thickness={5} openness={90}
        style={{ stroke: "#51452D", strokeWidth: 0.5 }}
        step={3}
      />
      <TickRing
        cx={50} cy={50} radius={50} thickness={5} openness={90}
        style={{ stroke: "#FCC969", strokeWidth: 1 }}
        count={3}
      />
    </g>
  );
}
```

### Step 2: Value Indicator

Add a ValueRing so the current value is visible. Place it so it sits above the scale rings but below the background image and indicator (layer order matters).

```tsx title="HifiKnobStep2.tsx" showLineNumbers
import { TickRing, ValueRing } from "@cutoff/audio-ui-react";

function HifiKnobView({ normalizedValue }: ControlComponentViewProps) {
  return (
    <g>
      <TickRing
        cx={50} cy={50} radius={50} thickness={5} openness={90}
        style={{ stroke: "#51452D", strokeWidth: 0.5 }}
        step={3}
      />
      <TickRing
        cx={50} cy={50} radius={50} thickness={5} openness={90}
        style={{ stroke: "#FCC969", strokeWidth: 1 }}
        count={3}
      />
      <ValueRing
        cx={50}
        cy={50}
        radius={43}
        normalizedValue={normalizedValue}
        thickness={1}
        roundness={true}
        openness={90}
        fgArcStyle={{ stroke: "#FCC969" }}
        bgArcStyle={{ stroke: "currentColor", opacity: 0.15 }}
      />
    </g>
  );
}
```

### Step 3: Background Image

Add a static background with RadialImage. This gives the knob its “face” (e.g. metal texture). The image is drawn behind the value arc and scale.

```tsx title="HifiKnobStep3.tsx" showLineNumbers
import { TickRing, ValueRing, RadialImage } from "@cutoff/audio-ui-react";

function HifiKnobView({ normalizedValue }: ControlComponentViewProps) {
  return (
    <g>
      <TickRing
        cx={50} cy={50} radius={50} thickness={5} openness={90}
        style={{ stroke: "#51452D", strokeWidth: 0.5 }}
        step={3}
      />
      <TickRing
        cx={50} cy={50} radius={50} thickness={5} openness={90}
        style={{ stroke: "#FCC969", strokeWidth: 1 }}
        count={3}
      />
      <ValueRing
        cx={50} cy={50} radius={43}
        normalizedValue={normalizedValue}
        thickness={1} roundness={true} openness={90}
        fgArcStyle={{ stroke: "#FCC969" }}
        bgArcStyle={{ stroke: "currentColor", opacity: 0.15 }}
      />
      <RadialImage
        cx={50} cy={50} radius={40}
        imageHref="/images/demo/knob-metal.png"
      />
    </g>
  );
}
```

### Step 4: Rotating Indicator

Add a RotaryImage with a line (or any SVG) as the needle. It rotates with `normalizedValue` and completes the Hi-Fi look.

```tsx title="HifiKnobStep4.tsx" showLineNumbers
import {
  TickRing,
  ValueRing,
  RadialImage,
  RotaryImage,
} from "@cutoff/audio-ui-react";

function HifiKnobView({ normalizedValue }: ControlComponentViewProps) {
  return (
    <g>
      <TickRing
        cx={50} cy={50} radius={50} thickness={5} openness={90}
        style={{ stroke: "#51452D", strokeWidth: 0.5 }}
        step={3}
      />
      <TickRing
        cx={50} cy={50} radius={50} thickness={5} openness={90}
        style={{ stroke: "#FCC969", strokeWidth: 1 }}
        count={3}
      />
      <ValueRing
        cx={50} cy={50} radius={43}
        normalizedValue={normalizedValue}
        thickness={1} roundness={true} openness={90}
        fgArcStyle={{ stroke: "#FCC969" }}
        bgArcStyle={{ stroke: "currentColor", opacity: 0.15 }}
      />
      <RadialImage
        cx={50} cy={50} radius={40}
        imageHref="/images/demo/knob-metal.png"
      />
      <RotaryImage
        cx={50} cy={50} radius={43}
        normalizedValue={normalizedValue}
        openness={90}
      >
        <line
          x1="43" y1="15" x2="43" y2="8"
          stroke="white" strokeWidth={2} strokeLinecap="round"
        />
      </RotaryImage>
    </g>
  );
}
```

### Complete Example

Attach the view to [ContinuousControl](https://cutoff.dev/audio-ui/docs/latest/advanced-customization/continuous-control-primitive) and set `viewBox`, `labelHeightUnits`, and `interaction` on the view so the Control Primitive (ContinuousControl here) knows how to create the actual SVG parent component and which interactions to set on the AdaptiveBox containing it. Assuming `HifiKnobView` is already defined (e.g. from the steps above), wire it up like this:

```tsx title="HifiKnobUsage.tsx" showLineNumbers
import { useState } from "react";
import { ContinuousControl } from "@cutoff/audio-ui-react";
// HifiKnobView defined elsewhere (view + viewBox, labelHeightUnits, interaction)

export default function HifiKnobUsage() {
  const [value, setValue] = useState(0);
  return (
    <ContinuousControl
      view={HifiKnobView}
      value={value}
      onChange={(e) => setValue(e.value)}
      min={-60}
      max={6}
      unit="dB"
      scale="log"
      label="Hi-Fi Knob"
      valueAsLabel="interactive"
    />
  );
}
```

The demo at the top of this page is implemented this way; the source lives in the cutoff-dev app as a demo component (`HifiKnobDemo`).

### Passing props to the view

You can define additional props on your view component and pass them via the `viewProps` prop. The primitive forwards these to the view alongside `normalizedValue`, `className`, and `style`:

```tsx
// View accepts an optional accentColor
function MyKnobView({ normalizedValue, accentColor }: ControlComponentViewProps & { accentColor?: string }) {
  return (/* ... */);
}

<ContinuousControl
  view={MyKnobView}
  viewProps={{ accentColor: "#FCC969" }}
  value={value}
  onChange={(e) => setValue(e.value)}
  min={-60}
  max={6}
  unit="dB"
  scale="log"
  label="Knob"
  valueAsLabel="interactive"
/>
```

> **More examples:** For other customization examples (Guitar Knob, Selector Knob, etc.), see the source code of the audio-ui playground: [apps/playground-react/components/examples](https://github.com/cutoff/audio-ui/tree/main/apps/playground-react/components/examples).

## Composing Custom Sliders

Building a custom slider follows a similar pattern:

### Step 1: Background Track

```tsx title="SliderStep1.tsx" showLineNumbers
function CustomSliderView({ normalizedValue }: ControlComponentViewProps) {
  return (
    <LinearStrip
      cx={50}
      cy={150}
      length={260}
      thickness={6}
      roundness={0.3}
    />
  );
}
```

### Step 2: Value Fill

```tsx title="SliderStep2.tsx" showLineNumbers
function CustomSliderView({ normalizedValue }: ControlComponentViewProps) {
  return (
    <g>
      <LinearStrip cx={50} cy={150} length={260} thickness={6} roundness={0.3} />
      <ValueStrip
        cx={50}
        cy={150}
        length={260}
        thickness={6}
        normalizedValue={normalizedValue}
        roundness={0.3}
      />
    </g>
  );
}
```

### Step 3: Cursor

```tsx title="SliderStep3.tsx" showLineNumbers
function CustomSliderView({ normalizedValue }: ControlComponentViewProps) {
  return (
    <g>
      <LinearStrip cx={50} cy={150} length={260} thickness={6} roundness={0.3} />
      <ValueStrip
        cx={50}
        cy={150}
        length={260}
        thickness={6}
        normalizedValue={normalizedValue}
        roundness={0.3}
      />
      <LinearCursor
        cx={50}
        cy={150}
        length={260}
        normalizedValue={normalizedValue}
        width={10}
        aspectRatio={1}
        roundness={1}
      />
    </g>
  );
}
```

### Complete Slider Example

```tsx title="CompleteCustomSlider.tsx" showLineNumbers
import {
  ContinuousControl,
  ControlComponentViewProps,
  LinearStrip,
  ValueStrip,
  LinearCursor,
} from "@cutoff/audio-ui-react";
import { useState } from "react";

function CustomSliderView({ normalizedValue }: ControlComponentViewProps) {
  return (
    <g>
      <LinearStrip
        cx={50}
        cy={150}
        length={260}
        thickness={6}
        roundness={0.3}
        style={{ fill: "rgba(0,0,0,0.1)" }}
      />
      <ValueStrip
        cx={50}
        cy={150}
        length={260}
        thickness={6}
        normalizedValue={normalizedValue}
        roundness={0.3}
        style={{ fill: "currentColor" }}
      />
      <LinearCursor
        cx={50}
        cy={150}
        length={260}
        normalizedValue={normalizedValue}
        width={12}
        aspectRatio={1.2}
        roundness={0.5}
        style={{ fill: "currentColor", stroke: "white", strokeWidth: 2 }}
      />
    </g>
  );
}

CustomSliderView.viewBox = { width: 100, height: 300 };
CustomSliderView.labelHeightUnits = 15;
CustomSliderView.interaction = {
  mode: "both",
  direction: "vertical",
};

export default function CompleteCustomSliderExample() {
  const [value, setValue] = useState(0.5);

  return (
    <ContinuousControl
      view={CustomSliderView}
      value={value}
      onChange={(e) => setValue(e.value)}
      min={0}
      max={1}
      label="Custom Slider"
    />
  );
}
```

## Rotation Behavior

All vector primitives use consistent rotation semantics:

* **Positive rotation**: Counter-clockwise (left)
* **Negative rotation**: Clockwise (right)

For horizontal orientation (vertical strip rotated to horizontal), use `rotation={-90}` or `rotation={270}`:

```tsx title="HorizontalSlider.tsx" showLineNumbers
<LinearStrip
  cx={150}
  cy={50}
  length={260}
  thickness={6}
  rotation={-90} // Rotate to horizontal
/>
```

## Center Content

Center content is text or icons displayed in the middle of a radial or linear control (e.g. current value and unit). The **control primitive** renders it as an HTML overlay above the SVG view, not inside the view component.

Example: a minimal knob with the current value and unit in the center. The view draws only the ring; the value and "dB" are passed as `children` to `ContinuousControl`:

```tsx title="CenterContentExample.tsx" showLineNumbers
import { useState } from "react";
import {
  ContinuousControl,
  ValueRing,
  type ControlComponentViewProps,
  type ControlComponent,
} from "@cutoff/audio-ui-react";

function SimpleKnobView({ normalizedValue }: ControlComponentViewProps) {
  return (
    <g>
      <ValueRing
        cx={50}
        cy={50}
        radius={45}
        normalizedValue={normalizedValue}
        thickness={4}
        openness={90}
        fgArcStyle={{ stroke: "currentColor" }}
        bgArcStyle={{ stroke: "rgba(0,0,0,0.2)" }}
      />
    </g>
  );
}

SimpleKnobView.viewBox = { width: 100, height: 100 };
SimpleKnobView.labelHeightUnits = 10;
SimpleKnobView.interaction = { mode: "both", direction: "circular" };

export default function KnobWithCenterContent() {
  const [value, setValue] = useState(-12);
  const formattedValue = `${value.toFixed(1)}`;

  return (
    <ContinuousControl
      view={SimpleKnobView as ControlComponent}
      value={value}
      onChange={(e) => setValue(e.value)}
      min={-60}
      max={6}
      unit="dB"
      scale="log"
      label="Gain"
    >
      <div style={{ fontSize: "20px", fontWeight: 700 }}>{formattedValue}</div>
      <div style={{ fontSize: "10px", opacity: 0.6 }}>dB</div>
    </ContinuousControl>
  );
}
```

> **Important:** Do **not** render center content (text, icons) inside the SVG view component. Pass it as `children` (or `htmlOverlay`) to the control primitive so it is rendered as an HTML overlay for Safari compatibility. For the `htmlOverlay` prop and full API details, see [ContinuousControl – Center Content](https://cutoff.dev/audio-ui/docs/latest/advanced-customization/continuous-control-primitive#center-content-html-overlay).

## Performance Considerations

* **Memoization**: Primitives are memoized with `React.memo`
* **Single-path rendering**: TickRing uses optimized single-path rendering when possible
* **GPU acceleration**: CSS transforms and SVG rendering are hardware-accelerated
* **DOM optimization**: Avoid unnecessary `<g>` wrappers for single children

## Next Steps

* Explore [ContinuousControl Primitive](https://cutoff.dev/audio-ui/docs/latest/advanced-customization/continuous-control-primitive) for building custom continuous controls
* Check out [DiscreteControl Primitive](https://cutoff.dev/audio-ui/docs/latest/advanced-customization/discrete-control-primitive) for custom discrete controls
* Learn about [BooleanControl Primitive](https://cutoff.dev/audio-ui/docs/latest/advanced-customization/boolean-control-primitive) for custom boolean controls


---

# ContinuousControl Primitive

# ContinuousControl Primitive

> Complete API reference for ContinuousControl - the primitive for building custom continuous value controls.

ContinuousControl is the primitive component for building custom continuous value controls. It handles all interaction logic, parameter management, and layout while you define the visual appearance.

## API Reference

| Name                   | Type                                                                 | Default    | Required | Description                                                      |
| :--------------------- | :------------------------------------------------------------------- | :--------- | :------- | :--------------------------------------------------------------- |
| view                   | ControlComponent\<P>                                                 | -          | Yes      | The visualization component that implements ControlComponentView |
| viewProps              | P                                                                    | -          | No       | Props specific to the view component                             |
| value                  | number                                                               | -          | Yes      | Current value of the control                                     |
| onChange               | (event: AudioControlEvent\<number>) => void                          | -          | No       | Handler for value changes                                        |
| min                    | number                                                               | -          | No       | Minimum value (ad-hoc mode)                                      |
| max                    | number                                                               | -          | No       | Maximum value (ad-hoc mode)                                      |
| step                   | number                                                               | -          | No       | Step size for value adjustments (ad-hoc mode)                    |
| bipolar                | boolean                                                              | false      | No       | Whether the component operates in bipolar mode                   |
| parameter              | ContinuousParameter                                                  | -          | No       | Audio Parameter definition (overrides ad-hoc props)              |
| label                  | string                                                               | -          | No       | Label displayed below the component                              |
| valueFormatter         | (value: number, parameterDef: AudioParameter) => string \| undefined | -          | No       | Custom renderer for the value display                            |
| valueAsLabel           | "labelOnly" \| "valueOnly" \| "interactive"                          | labelOnly  | No       | Controls how label and value are displayed                       |
| interactionMode        | "drag" \| "wheel" \| "both"                                          | both       | No       | Interaction mode                                                 |
| interactionDirection   | "vertical" \| "horizontal" \| "circular" \| "both"                   | -          | No       | Direction of interaction                                         |
| interactionSensitivity | number                                                               | -          | No       | Sensitivity multiplier for interactions                          |
| viewBoxWidthUnits      | number                                                               | -          | No       | Override viewBox width from view component                       |
| viewBoxHeightUnits     | number                                                               | -          | No       | Override viewBox height from view component                      |
| labelHeightUnits       | number                                                               | -          | No       | Override label height from view component                        |
| displayMode            | "scaleToFit" \| "fill"                                               | scaleToFit | No       | How the component scales within its container                    |
| labelMode              | "visible" \| "hidden" \| "none"                                      | visible    | No       | Label display mode                                               |
| labelPosition          | "above" \| "below"                                                   | below      | No       | Label position relative to control                               |
| labelAlign             | "start" \| "center" \| "end"                                         | center     | No       | Label horizontal alignment                                       |
| className              | string                                                               | -          | No       | Additional CSS classes                                           |
| style                  | React.CSSProperties                                                  | -          | No       | Additional inline styles                                         |

## View Component Contract

Your view component must implement the `ControlComponentView` interface:

```typescript
interface ControlComponentView {
  viewBox: { width: number; height: number };
  labelHeightUnits?: number; // Default: 20
  interaction?: {
    mode: "drag" | "wheel" | "both";
    direction: "vertical" | "horizontal" | "circular" | "both";
  };
}
```

### Static Properties

Define these as static properties on your view component:

```tsx title="ViewComponentStatic.tsx" showLineNumbers
function MyCustomView({ normalizedValue }: ControlComponentViewProps) {
  // Your visualization
  return <g>{/* SVG content */}</g>;
}

// Required
MyCustomView.viewBox = { width: 100, height: 100 };

// Optional (provide defaults)
MyCustomView.labelHeightUnits = 15;
MyCustomView.interaction = {
  mode: "both",
  direction: "circular",
};
```

### View Props

Your view component receives these props:

```typescript
type ControlComponentViewProps<P = Record<string, unknown>> = {
  normalizedValue: number; // 0.0 to 1.0
  children?: React.ReactNode; // For center content (HTML overlay)
  className?: string;
  style?: React.CSSProperties;
  // ... any custom props from viewProps
} & P;
```

## Basic Usage

```tsx title="BasicUsage.tsx" showLineNumbers
import { ContinuousControl, ControlComponentViewProps } from "@cutoff/audio-ui-react";
import { useState } from "react";

function SimpleKnobView({ normalizedValue }: ControlComponentViewProps) {
  const angle = normalizedValue * 270 - 135;
  const radians = (angle * Math.PI) / 180;

  return (
    <g>
      <circle cx="50" cy="50" r="45" fill="none" stroke="#ccc" strokeWidth="4" />
      <line
        x1="50"
        y1="50"
        x2={50 + 40 * Math.cos(radians)}
        y2={50 + 40 * Math.sin(radians)}
        stroke="currentColor"
        strokeWidth="3"
      />
    </g>
  );
}

SimpleKnobView.viewBox = { width: 100, height: 100 };

export default function BasicUsageExample() {
  const [value, setValue] = useState(0.5);

  return (
    <ContinuousControl
      view={SimpleKnobView}
      value={value}
      onChange={(e) => setValue(e.value)}
      min={0}
      max={1}
      label="Custom Knob"
    />
  );
}
```

## Parameter Integration

### Ad-Hoc Mode

Provide `min`, `max`, and optional `step` directly:

```tsx title="AdHocMode.tsx" showLineNumbers
<ContinuousControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  min={20}
  max={20000}
  step={1}
  label="Frequency"
/>
```

### Parameter Model Mode

Provide a full `ContinuousParameter`:

```tsx title="ParameterMode.tsx" showLineNumbers
import { createContinuousParameter } from "@cutoff/audio-ui-react";

const cutoffParam = createContinuousParameter({
  paramId: "cutoff",
  label: "Cutoff",
  min: 20,
  max: 20000,
  unit: "Hz",
  scale: "log",
  defaultValue: 1000,
});

<ContinuousControl
  view={MyView}
  parameter={cutoffParam}
  value={value}
  onChange={(e) => setValue(e.value)}
/>
```

## Interaction Modes

### Interaction Mode

Control which input methods are enabled:

```tsx title="InteractionMode.tsx" showLineNumbers
// Drag only
<ContinuousControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  interactionMode="drag"
/>

// Wheel only
<ContinuousControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  interactionMode="wheel"
/>

// Both (default)
<ContinuousControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  interactionMode="both"
/>
```

### Interaction Direction

Control how drag interactions map to value changes:

```tsx title="InteractionDirection.tsx" showLineNumbers
// Circular (for knobs)
<ContinuousControl
  view={KnobView}
  value={value}
  onChange={(e) => setValue(e.value)}
  interactionDirection="circular"
/>

// Vertical (for vertical sliders)
<ContinuousControl
  view={VerticalSliderView}
  value={value}
  onChange={(e) => setValue(e.value)}
  interactionDirection="vertical"
/>

// Horizontal (for horizontal sliders)
<ContinuousControl
  view={HorizontalSliderView}
  value={value}
  onChange={(e) => setValue(e.value)}
  interactionDirection="horizontal"
/>
```

### Interaction Sensitivity

Tune the responsiveness of interactions:

```tsx title="InteractionSensitivity.tsx" showLineNumbers
// Low sensitivity (fine control)
<ContinuousControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  interactionSensitivity={0.001}
/>

// High sensitivity (coarse control)
<ContinuousControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  interactionSensitivity={0.01}
/>
```

**Default sensitivity**: `0.005` (200px throw for full range)

## Custom View Props

Pass custom props to your view component via `viewProps`:

```tsx title="CustomViewProps.tsx" showLineNumbers
function CustomView({
  normalizedValue,
  customColor,
  customThickness,
}: ControlComponentViewProps<{
  customColor: string;
  customThickness: number;
}>) {
  return (
    <g>
      <circle
        cx="50"
        cy="50"
        r="45"
        fill="none"
        stroke={customColor}
        strokeWidth={customThickness}
      />
    </g>
  );
}

CustomView.viewBox = { width: 100, height: 100 };

<ContinuousControl
  view={CustomView}
  viewProps={{
    customColor: "#3b82f6",
    customThickness: 4,
  }}
  value={value}
  onChange={(e) => setValue(e.value)}
/>
```

## Center Content (HTML Overlay)

Render HTML content (text, icons) as an overlay above the SVG:

```tsx title="CenterContent.tsx" showLineNumbers
<ContinuousControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  htmlOverlay={
    <div style={{ fontSize: "20px", fontWeight: 700 }}>
      {formattedValue}
    </div>
  }
/>
```

Or use the `children` prop for a simpler API:

```tsx title="ChildrenContent.tsx" showLineNumbers
<ContinuousControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
>
  <div style={{ fontSize: "20px", fontWeight: 700 }}>
    {formattedValue}
  </div>
  <div style={{ fontSize: "10px", opacity: 0.6 }}>dB</div>
</ContinuousControl>
```

**Why HTML overlay?** Safari has rendering bugs with SVG `foreignObject` in complex layouts. The overlay approach renders HTML outside the SVG for compatibility.

## Value Display Modes

Control how the label and value are displayed:

```tsx title="ValueAsLabel.tsx" showLineNumbers
// Label only (default)
<ContinuousControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  valueAsLabel="labelOnly"
/>

// Value only
<ContinuousControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  valueAsLabel="valueOnly"
/>

// Interactive (shows value during interaction)
<ContinuousControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  valueAsLabel="interactive"
/>
```

## Bipolar Mode

For controls centered around zero (pan, balance):

```tsx title="Bipolar.tsx" showLineNumbers
<ContinuousControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  min={-100}
  max={100}
  bipolar={true}
/>
```

In bipolar mode:

* Default value is center (0.5 normalized)
* Visual rendering starts from center
* Value indicators fill from center

## Overriding ViewBox Dimensions

Override the default viewBox from your view component:

```tsx title="OverrideViewBox.tsx" showLineNumbers
<ContinuousControl
  view={MyView} // MyView.viewBox = { width: 100, height: 100 }
  viewBoxWidthUnits={150} // Override to 150
  viewBoxHeightUnits={150} // Override to 150
  value={value}
  onChange={(e) => setValue(e.value)}
/>
```

## Layout Integration

ContinuousControl uses AdaptiveBox for layout. All AdaptiveBox props are supported:

```tsx title="LayoutProps.tsx" showLineNumbers
<ContinuousControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  displayMode="fill" // or "scaleToFit"
  labelMode="visible" // or "hidden", "none"
  labelPosition="above" // or "below"
  labelAlign="start" // or "center", "end"
  labelHeightUnits={20} // Override label height
/>
```

## Advanced Usage Patterns

### Custom Formatter

Provide a custom value formatter:

```tsx title="CustomFormatter.tsx" showLineNumbers
<ContinuousControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  valueFormatter={(value, paramDef) => {
    if (paramDef.unit === "Hz") {
      return frequencyFormatter(value);
    }
    return `${value.toFixed(1)}${paramDef.unit || ""}`;
  }}
/>
```

### Double-Click Reset

ContinuousControl supports double-click to reset to default value when `onChange` is provided:

```tsx title="DoubleClickReset.tsx" showLineNumbers
<ContinuousControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  parameter={createContinuousParameter({
    // ...
    defaultValue: 1000, // Resets to this value on double-click
  })}
/>
```

### Keyboard Support

Automatic keyboard support:

* **Arrow keys**: Increment/decrement (clamped at min/max)
* **Home/End**: Jump to min/max
* **Tab**: Focus management

## Performance Considerations

* **Memoization**: Both ContinuousControl and view components are memoized
* **Event handlers**: Use `useRef` for mutable state to avoid stale closures
* **Re-renders**: Only re-renders when props change (value, viewProps, etc.)
* **Interaction**: Global event listeners only attached during active drag

## Type Exports

```typescript
import {
  ContinuousControl,
  ContinuousControlComponentProps,
  ControlComponentView,
  ControlComponentViewProps,
} from "@cutoff/audio-ui-react";
```

## Examples

See the [View System and View Primitives](https://cutoff.dev/audio-ui/docs/latest/advanced-customization/view-system-view-primitives) guide for complete examples of building custom controls with ContinuousControl.

## Next Steps

* Learn about [DiscreteControl Primitive](https://cutoff.dev/audio-ui/docs/latest/advanced-customization/discrete-control-primitive) for discrete value controls
* Explore [BooleanControl Primitive](https://cutoff.dev/audio-ui/docs/latest/advanced-customization/boolean-control-primitive) for boolean controls
* Check out [View Primitives](https://cutoff.dev/audio-ui/docs/latest/advanced-customization/view-system-view-primitives) for building custom visualizations


---

# DiscreteControl Primitive

# DiscreteControl Primitive

> Complete API reference for DiscreteControl - the primitive for building custom discrete/enum value controls.

DiscreteControl is the primitive component for building custom discrete/enum value controls. It handles option management, value resolution, interaction, and layout while you define the visual appearance.

## API Reference

| Name               | Type                                                                   | Default    | Required | Description                                                      |
| :----------------- | :--------------------------------------------------------------------- | :--------- | :------- | :--------------------------------------------------------------- |
| view               | ControlComponent\<P>                                                   | -          | Yes      | The visualization component that implements ControlComponentView |
| viewProps          | P                                                                      | -          | No       | Props specific to the view component                             |
| value              | string \| number                                                       | -          | No       | Current value (controlled mode)                                  |
| defaultValue       | string \| number                                                       | -          | No       | Default value (uncontrolled mode)                                |
| onChange           | (event: AudioControlEvent\<string \| number>) => void                  | -          | No       | Handler for value changes                                        |
| options            | Array<{ value: string \| number; label?: string; midiValue?: number }> | -          | No       | Option definitions for parameter model (ad-hoc mode)             |
| children           | React.ReactNode                                                        | -          | No       | OptionView children for visual content (ad-hoc or hybrid mode)   |
| parameter          | DiscreteParameter                                                      | -          | No       | Audio Parameter definition (overrides ad-hoc props)              |
| label              | string                                                                 | -          | No       | Label displayed below the component                              |
| viewBoxWidthUnits  | number                                                                 | -          | No       | Override viewBox width from view component                       |
| viewBoxHeightUnits | number                                                                 | -          | No       | Override viewBox height from view component                      |
| labelHeightUnits   | number                                                                 | -          | No       | Override label height from view component                        |
| displayMode        | "scaleToFit" \| "fill"                                                 | scaleToFit | No       | How the component scales within its container                    |
| labelMode          | "visible" \| "hidden" \| "none"                                        | visible    | No       | Label display mode                                               |
| labelPosition      | "above" \| "below"                                                     | below      | No       | Label position relative to control                               |
| labelAlign         | "start" \| "center" \| "end"                                           | center     | No       | Label horizontal alignment                                       |
| className          | string                                                                 | -          | No       | Additional CSS classes                                           |
| style              | React.CSSProperties                                                    | -          | No       | Additional inline styles                                         |

## Options vs Children: Critical Distinction

Understanding the difference between `options` and `children` is essential:

* **`options` prop**: Defines the **parameter model** (value, label, midiValue). This is the data definition.
* **`children` (OptionView)**: Provides **visual content** (ReactNodes) for rendering. This is the visual representation.

These serve different purposes and can be used together.

## Modes of Operation

DiscreteControl supports four modes:

### Mode 1: Ad-Hoc with Options Prop

Model from `options`, visual from `children` (if provided) or default rendering:

```tsx title="AdHocOptions.tsx" showLineNumbers
import { DiscreteControl, OptionView } from "@cutoff/audio-ui-react";
import { useState } from "react";

export default function AdHocOptionsExample() {
  const [value, setValue] = useState("sine");

  return (
    <DiscreteControl
      view={MyView}
      value={value}
      onChange={(e) => setValue(e.value)}
      options={[
        { value: "sine", label: "Sine" },
        { value: "square", label: "Square" },
        { value: "sawtooth", label: "Saw" },
      ]}
    >
      {/* Optional: Custom visual content */}
      <OptionView value="sine">Sine</OptionView>
      <OptionView value="square">Square</OptionView>
      <OptionView value="sawtooth">Saw</OptionView>
    </DiscreteControl>
  );
}
```

### Mode 2: Ad-Hoc with Children Only

Model inferred from OptionView children, visual from children:

```tsx title="AdHocChildren.tsx" showLineNumbers
import { DiscreteControl, OptionView } from "@cutoff/audio-ui-react";
import { useState } from "react";

export default function AdHocChildrenExample() {
  const [value, setValue] = useState("sine");

  return (
    <DiscreteControl
      view={MyView}
      value={value}
      onChange={(e) => setValue(e.value)}
    >
      <OptionView value="sine">Sine</OptionView>
      <OptionView value="square">Square</OptionView>
      <OptionView value="sawtooth">Saw</OptionView>
    </DiscreteControl>
  );
}
```

### Mode 3: Strict Mode (Parameter Only)

Model from `parameter`, visual via `renderOption` callback (not supported in current API - use Mode 4 instead):

```tsx title="StrictMode.tsx" showLineNumbers
import { createDiscreteParameter } from "@cutoff/audio-ui-react";
import { useState } from "react";

const waveformParam = createDiscreteParameter({
  paramId: "waveform",
  label: "Waveform",
  options: [
    { value: "sine", label: "Sine" },
    { value: "square", label: "Square" },
    { value: "sawtooth", label: "Saw" },
  ],
  defaultValue: "sine",
});

export default function StrictModeExample() {
  const [value, setValue] = useState("sine");

  return (
    <DiscreteControl
      view={MyView}
      parameter={waveformParam}
      value={value}
      onChange={(e) => setValue(e.value)}
    />
  );
}
```

### Mode 4: Hybrid Mode (Parameter + Children)

Model from `parameter`, visual from children (matched by value):

```tsx title="HybridMode.tsx" showLineNumbers
import { createDiscreteParameter, OptionView } from "@cutoff/audio-ui-react";
import { useState } from "react";

const waveformParam = createDiscreteParameter({
  paramId: "waveform",
  label: "Waveform",
  options: [
    { value: "sine", label: "Sine" },
    { value: "square", label: "Square" },
    { value: "sawtooth", label: "Saw" },
  ],
  defaultValue: "sine",
});

export default function HybridModeExample() {
  const [value, setValue] = useState("sine");

  return (
    <DiscreteControl
      view={MyView}
      parameter={waveformParam}
      value={value}
      onChange={(e) => setValue(e.value)}
    >
      {/* Visual content matched by value */}
      <OptionView value="sine">
        <SineIcon />
      </OptionView>
      <OptionView value="square">
        <SquareIcon />
      </OptionView>
      <OptionView value="sawtooth">
        <SawIcon />
      </OptionView>
    </DiscreteControl>
  );
}
```

## View Component Contract

Your view component must implement the `ControlComponentView` interface:

```typescript
interface ControlComponentView {
  viewBox: { width: number; height: number };
  labelHeightUnits?: number; // Default: 20
  interaction?: {
    mode: "drag" | "wheel" | "both";
    direction: "vertical" | "horizontal" | "circular" | "both";
  };
}
```

### View Props

Your view component receives these props:

```typescript
type ControlComponentViewProps<P = Record<string, unknown>> = {
  normalizedValue: number; // 0.0 to 1.0 (maps to current option index)
  children?: React.ReactNode; // Visual content for current option
  className?: string;
  style?: React.CSSProperties;
  // ... any custom props from viewProps
} & P;
```

**Important**: `normalizedValue` represents the current option index normalized to 0-1, not the option value itself. Use it to position indicators or highlight the current selection.

## Basic Usage

```tsx title="BasicUsage.tsx" showLineNumbers
import { DiscreteControl, OptionView, ControlComponentViewProps } from "@cutoff/audio-ui-react";
import { useState } from "react";

function SimpleSelectorView({ normalizedValue, children }: ControlComponentViewProps) {
  const positions = 4;
  const currentPosition = Math.round(normalizedValue * (positions - 1));
  const angle = (currentPosition / positions) * 360;

  return (
    <g>
      <circle cx="50" cy="50" r="45" fill="none" stroke="#ccc" strokeWidth="4" />
      <line
        x1="50"
        y1="50"
        x2={50 + 35 * Math.cos((angle * Math.PI) / 180)}
        y2={50 + 35 * Math.sin((angle * Math.PI) / 180)}
        stroke="currentColor"
        strokeWidth="4"
      />
    </g>
  );
}

SimpleSelectorView.viewBox = { width: 100, height: 100 };

export default function BasicUsageExample() {
  const [value, setValue] = useState("option1");

  return (
    <DiscreteControl
      view={SimpleSelectorView}
      value={value}
      onChange={(e) => setValue(e.value)}
      label="Selector"
    >
      <OptionView value="option1">1</OptionView>
      <OptionView value="option2">2</OptionView>
      <OptionView value="option3">3</OptionView>
      <OptionView value="option4">4</OptionView>
    </DiscreteControl>
  );
}
```

## Parameter Integration

### Ad-Hoc Mode

Provide options directly:

```tsx title="AdHocParameter.tsx" showLineNumbers
<DiscreteControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  options={[
    { value: "sine", label: "Sine" },
    { value: "square", label: "Square" },
    { value: "sawtooth", label: "Saw" },
  ]}
/>
```

### Parameter Model Mode

Provide a full `DiscreteParameter`:

```tsx title="ParameterModel.tsx" showLineNumbers
import { createDiscreteParameter } from "@cutoff/audio-ui-react";

const waveformParam = createDiscreteParameter({
  paramId: "waveform",
  label: "Waveform",
  options: [
    { value: "sine", label: "Sine" },
    { value: "square", label: "Square" },
    { value: "sawtooth", label: "Saw" },
  ],
  defaultValue: "sine",
});

<DiscreteControl
  view={MyView}
  parameter={waveformParam}
  value={value}
  onChange={(e) => setValue(e.value)}
/>
```

## Visual Content with Children

Use `OptionView` children to provide custom visual content:

```tsx title="VisualContent.tsx" showLineNumbers
<DiscreteControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
>
  <OptionView value="sine">
    <SineIcon className="w-6 h-6" />
  </OptionView>
  <OptionView value="square">
    <SquareIcon className="w-6 h-6" />
  </OptionView>
  <OptionView value="sawtooth">
    <SawIcon className="w-6 h-6" />
  </OptionView>
</DiscreteControl>
```

The view component receives the current option's visual content via the `children` prop.

## Interaction

DiscreteControl supports:

* **Click**: Cycles to next value (wraps to first when reaching end)
* **Space/Enter**: Cycles to next value (same as click)
* **Arrow Keys**: Step increment/decrement (clamped at min/max, no wrap)

```tsx title="Interaction.tsx" showLineNumbers
<DiscreteControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  // Interaction is automatic - no configuration needed
/>
```

## Value Resolution

When the current value doesn't match any option, DiscreteControl automatically finds the nearest valid option:

```tsx title="ValueResolution.tsx" showLineNumbers
// If value is "invalid", DiscreteControl finds nearest match
<DiscreteControl
  view={MyView}
  value="invalid" // Will resolve to nearest option
  onChange={(e) => setValue(e.value)}
  options={[
    { value: "sine", label: "Sine" },
    { value: "square", label: "Square" },
  ]}
/>
```

## Custom View Props

Pass custom props to your view component:

```tsx title="CustomViewProps.tsx" showLineNumbers
function CustomView({
  normalizedValue,
  children,
  customColor,
}: ControlComponentViewProps<{ customColor: string }>) {
  return (
    <g>
      <circle
        cx="50"
        cy="50"
        r="45"
        fill="none"
        stroke={customColor}
        strokeWidth="4"
      />
      {/* Render current option's visual content */}
      <g transform="translate(50, 50)">{children}</g>
    </g>
  );
}

CustomView.viewBox = { width: 100, height: 100 };

<DiscreteControl
  view={CustomView}
  viewProps={{ customColor: "#3b82f6" }}
  value={value}
  onChange={(e) => setValue(e.value)}
>
  <OptionView value="sine">Sine</OptionView>
  <OptionView value="square">Square</OptionView>
</DiscreteControl>
```

## Center Content (HTML Overlay)

Render HTML content as an overlay:

```tsx title="CenterContent.tsx" showLineNumbers
<DiscreteControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  htmlOverlay={
    <div style={{ fontSize: "20px", fontWeight: 700 }}>
      {currentOptionLabel}
    </div>
  }
/>
```

Or use the `children` prop pattern where children provide the visual content for the current option.

## MIDI Integration

DiscreteControl supports MIDI mapping strategies:

### Spread (Default)

Options distributed evenly across MIDI range:

```tsx title="MidiSpread.tsx" showLineNumbers
<DiscreteControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  options={[
    { value: "sine", label: "Sine" },
    { value: "square", label: "Square" },
    { value: "sawtooth", label: "Saw" },
  ]}
  midiMapping="spread"
  // Maps: 0 -> sine, 64 -> square, 127 -> sawtooth
/>
```

### Sequential

MIDI value equals option index:

```tsx title="MidiSequential.tsx" showLineNumbers
<DiscreteControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  options={[
    { value: "sine", label: "Sine" },
    { value: "square", label: "Square" },
    { value: "sawtooth", label: "Saw" },
  ]}
  midiMapping="sequential"
  // Maps: 0 -> sine, 1 -> square, 2 -> sawtooth
/>
```

### Custom

Use explicit `midiValue` in options:

```tsx title="MidiCustom.tsx" showLineNumbers
<DiscreteControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  options={[
    { value: "sine", label: "Sine", midiValue: 0 },
    { value: "square", label: "Square", midiValue: 42 },
    { value: "sawtooth", label: "Saw", midiValue: 84 },
  ]}
  midiMapping="custom"
/>
```

## Performance Considerations

* **O(1) lookups**: Value-to-index and option-by-value Maps for fast lookups
* **Memoization**: Both DiscreteControl and view components are memoized
* **Value resolution**: Automatic nearest-match resolution prevents invalid states
* **Re-renders**: Only re-renders when value or options change

## Advanced Usage Patterns

### Data-Driven Options

When options come from external sources:

```tsx title="DataDriven.tsx" showLineNumbers
const waveforms = [
  { value: "sine", label: "Sine", icon: SineIcon },
  { value: "square", label: "Square", icon: SquareIcon },
  { value: "sawtooth", label: "Saw", icon: SawIcon },
];

<DiscreteControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  options={waveforms.map((w) => ({ value: w.value, label: w.label }))}
>
  {waveforms.map((w) => (
    <OptionView key={w.value} value={w.value}>
      <w.icon />
    </OptionView>
  ))}
</DiscreteControl>
```

### Controlled vs Uncontrolled

```tsx title="ControlledUncontrolled.tsx" showLineNumbers
// Controlled (recommended)
const [value, setValue] = useState("sine");
<DiscreteControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
>
  <OptionView value="sine">Sine</OptionView>
</DiscreteControl>

// Uncontrolled
<DiscreteControl
  view={MyView}
  defaultValue="sine"
  onChange={(e) => console.log(e.value)}
>
  <OptionView value="sine">Sine</OptionView>
</DiscreteControl>
```

## Type Exports

```typescript
import {
  DiscreteControl,
  DiscreteControlComponentProps,
  OptionView,
  ControlComponentView,
  ControlComponentViewProps,
} from "@cutoff/audio-ui-react";
```

## Next Steps

* Learn about [ContinuousControl Primitive](https://cutoff.dev/audio-ui/docs/latest/advanced-customization/continuous-control-primitive) for continuous value controls
* Explore [BooleanControl Primitive](https://cutoff.dev/audio-ui/docs/latest/advanced-customization/boolean-control-primitive) for boolean controls
* Check out [View Primitives](https://cutoff.dev/audio-ui/docs/latest/advanced-customization/view-system-view-primitives) for building custom visualizations


---

# BooleanControl Primitive

# BooleanControl Primitive

> Complete API reference for BooleanControl - the primitive for building custom boolean (on/off) controls.

BooleanControl is the primitive component for building custom boolean (on/off) controls. It handles interaction logic, parameter management, and layout while you define the visual appearance.

## API Reference

| Name               | Type                                         | Default    | Required | Description                                                      |
| :----------------- | :------------------------------------------- | :--------- | :------- | :--------------------------------------------------------------- |
| view               | ControlComponent\<P>                         | -          | Yes      | The visualization component that implements ControlComponentView |
| viewProps          | P                                            | -          | No       | Props specific to the view component                             |
| value              | boolean                                      | -          | Yes      | Current value of the control                                     |
| onChange           | (event: AudioControlEvent\<boolean>) => void | -          | No       | Handler for value changes                                        |
| onClick            | React.MouseEventHandler                      | -          | No       | Click event handler (for non-editable views)                     |
| latch              | boolean                                      | false      | No       | Whether the control operates in latch (toggle) or momentary mode |
| parameter          | BooleanParameter                             | -          | No       | Audio Parameter definition (overrides ad-hoc props)              |
| label              | string                                       | -          | No       | Label displayed below the component                              |
| viewBoxWidthUnits  | number                                       | -          | No       | Override viewBox width from view component                       |
| viewBoxHeightUnits | number                                       | -          | No       | Override viewBox height from view component                      |
| labelHeightUnits   | number                                       | -          | No       | Override label height from view component                        |
| displayMode        | "scaleToFit" \| "fill"                       | scaleToFit | No       | How the component scales within its container                    |
| labelMode          | "visible" \| "hidden" \| "none"              | visible    | No       | Label display mode                                               |
| labelPosition      | "above" \| "below"                           | below      | No       | Label position relative to control                               |
| labelAlign         | "start" \| "center" \| "end"                 | center     | No       | Label horizontal alignment                                       |
| className          | string                                       | -          | No       | Additional CSS classes                                           |
| style              | React.CSSProperties                          | -          | No       | Additional inline styles                                         |

## View Component Contract

Your view component must implement the `ControlComponentView` interface:

```typescript
interface ControlComponentView {
  viewBox: { width: number; height: number };
  labelHeightUnits?: number; // Default: 20
  interaction?: {
    mode: "drag" | "wheel" | "both";
    direction: "vertical" | "horizontal" | "circular" | "both";
  };
}
```

### View Props

Your view component receives these props:

```typescript
type ControlComponentViewProps<P = Record<string, unknown>> = {
  normalizedValue: number; // 0.0 (false) or 1.0 (true)
  children?: React.ReactNode; // For center content (HTML overlay)
  className?: string;
  style?: React.CSSProperties;
  // ... any custom props from viewProps
} & P;
```

**Note**: `normalizedValue` is always `0.0` (false) or `1.0` (true) for boolean controls.

## Basic Usage

```tsx title="BasicUsage.tsx" showLineNumbers
import { BooleanControl, ControlComponentViewProps } from "@cutoff/audio-ui-react";
import { useState } from "react";

function SimpleSwitchView({ normalizedValue }: ControlComponentViewProps) {
  const isOn = normalizedValue > 0.5;

  return (
    <g>
      <rect
        x="20"
        y="40"
        width="60"
        height="20"
        rx="10"
        fill={isOn ? "currentColor" : "#ccc"}
      />
      <circle
        cx={isOn ? 70 : 30}
        cy="50"
        r="8"
        fill="white"
      />
    </g>
  );
}

SimpleSwitchView.viewBox = { width: 100, height: 100 };

export default function BasicUsageExample() {
  const [value, setValue] = useState(false);

  return (
    <BooleanControl
      view={SimpleSwitchView}
      value={value}
      onChange={(e) => setValue(e.value)}
      latch={true}
      label="Power"
    />
  );
}
```

## Latch vs Momentary Modes

### Latch Mode (Toggle)

In latch mode, the control toggles between on and off states:

```tsx title="LatchMode.tsx" showLineNumbers
<BooleanControl
  view={MyView}
  value={isOn}
  onChange={(e) => setIsOn(e.value)}
  latch={true}
  label="Power"
/>
```

**Behavior**:

* Click inside → toggles state
* Drag out while pressed → no change
* Drag back in while pressed → toggles again
* Works even when press starts outside the button

### Momentary Mode

In momentary mode, the control is only active while being pressed:

```tsx title="MomentaryMode.tsx" showLineNumbers
<BooleanControl
  view={MyView}
  value={isPressed}
  onChange={(e) => setIsPressed(e.value)}
  latch={false}
  label="Record"
/>
```

**Behavior**:

* Press inside → turns on
* Drag out while pressed → turns off
* Drag back in while pressed → turns on again
* Works even when press starts outside the button

## Drag-In/Drag-Out Behavior

BooleanControl supports hardware-like drag-in/drag-out interactions, enabling step sequencer patterns:

```tsx title="DragInOut.tsx" showLineNumbers
// Multiple buttons can be activated with a single drag gesture
<div style={{ display: "flex", gap: "10px" }}>
  {[0, 1, 2, 3, 4, 5, 6, 7].map((step) => (
    <BooleanControl
      key={step}
      view={StepButtonView}
      value={steps[step]}
      onChange={(e) => setSteps((prev) => ({ ...prev, [step]: e.value }))}
      latch={true}
      label={`Step ${step + 1}`}
    />
  ))}
</div>
```

**How it works**:

* Global pointer tracking (not just on button element)
* `mouseenter`/`mouseleave` events detect boundary crossing while pressed
* Enables classic step sequencer interactions

## Parameter Integration

### Ad-Hoc Mode

Provide `latch` directly:

```tsx title="AdHocMode.tsx" showLineNumbers
<BooleanControl
  view={MyView}
  value={isOn}
  onChange={(e) => setIsOn(e.value)}
  latch={true}
  label="Power"
/>
```

### Parameter Model Mode

Provide a full `BooleanParameter`:

```tsx title="ParameterMode.tsx" showLineNumbers
import { createBooleanParameter } from "@cutoff/audio-ui-react";

const powerParam = createBooleanParameter({
  paramId: "power",
  label: "Power",
  latch: true,
  defaultValue: false,
});

<BooleanControl
  view={MyView}
  parameter={powerParam}
  value={isOn}
  onChange={(e) => setIsOn(e.value)}
/>
```

## Interaction Modes

### Editable Control

When `onChange` is provided, the control is editable:

```tsx title="Editable.tsx" showLineNumbers
<BooleanControl
  view={MyView}
  value={isOn}
  onChange={(e) => setIsOn(e.value)} // Makes it editable
  latch={true}
/>
```

**Supported interactions**:

* Mouse click
* Touch tap
* Keyboard (Space/Enter)
* Drag-in/drag-out (momentary mode)

### Clickable View

When only `onClick` is provided (no `onChange`), the control is a clickable view:

```tsx title="Clickable.tsx" showLineNumbers
<BooleanControl
  view={MyView}
  value={false} // Value doesn't change
  onClick={() => handleAction()} // Triggers action
/>
```

**Behavior**:

* Triggers `onClick` on mouse click
* Touch events handled for mobile compatibility
* Value remains unchanged

### Both

When both `onChange` and `onClick` are provided:

```tsx title="Both.tsx" showLineNumbers
<BooleanControl
  view={MyView}
  value={isOn}
  onChange={(e) => setIsOn(e.value)} // Handles value changes
  onClick={() => handleClick()} // Also triggers on mouse click
  latch={true}
/>
```

**Behavior**:

* `onChange` handles touch events and value changes
* `onClick` triggers on mouse clicks
* Both can fire for the same interaction

## Custom View Props

Pass custom props to your view component:

```tsx title="CustomViewProps.tsx" showLineNumbers
function CustomView({
  normalizedValue,
  customColor,
  customSize,
}: ControlComponentViewProps<{
  customColor: string;
  customSize: number;
}>) {
  const isOn = normalizedValue > 0.5;

  return (
    <g>
      <circle
        cx="50"
        cy="50"
        r={customSize}
        fill={isOn ? customColor : "#ccc"}
      />
    </g>
  );
}

CustomView.viewBox = { width: 100, height: 100 };

<BooleanControl
  view={CustomView}
  viewProps={{
    customColor: "#3b82f6",
    customSize: 40,
  }}
  value={value}
  onChange={(e) => setValue(e.value)}
/>
```

## Center Content (HTML Overlay)

Render HTML content as an overlay:

```tsx title="CenterContent.tsx" showLineNumbers
<BooleanControl
  view={MyView}
  value={isOn}
  onChange={(e) => setIsOn(e.value)}
  htmlOverlay={
    <div style={{ fontSize: "16px", fontWeight: 700 }}>
      {isOn ? "ON" : "OFF"}
    </div>
  }
/>
```

Or use the `children` prop:

```tsx title="ChildrenContent.tsx" showLineNumbers
<BooleanControl
  view={MyView}
  value={isOn}
  onChange={(e) => setIsOn(e.value)}
>
  {isOn ? <OnIcon /> : <OffIcon />}
</BooleanControl>
```

## Keyboard Support

Automatic keyboard support:

* **Space/Enter**: Activates the control (toggles in latch mode, presses in momentary mode)
* **Tab**: Focus management

```tsx title="Keyboard.tsx" showLineNumbers
<BooleanControl
  view={MyView}
  value={isOn}
  onChange={(e) => setIsOn(e.value)}
  latch={true}
  // Keyboard support is automatic
/>
```

## Advanced Usage Patterns

### Step Sequencer Pattern

Use drag-in/drag-out to create step sequencer interactions:

```tsx title="StepSequencer.tsx" showLineNumbers
import { BooleanControl } from "@cutoff/audio-ui-react";
import { useState } from "react";

export default function StepSequencerExample() {
  const [steps, setSteps] = useState<boolean[]>([
    false, false, false, false,
    false, false, false, false,
  ]);

  return (
    <div style={{ display: "flex", gap: "4px" }}>
      {steps.map((active, index) => (
        <BooleanControl
          key={index}
          view={StepButtonView}
          value={active}
          onChange={(e) => {
            const newSteps = [...steps];
            newSteps[index] = e.value;
            setSteps(newSteps);
          }}
          latch={true}
        />
      ))}
    </div>
  );
}
```

### Toggle Group

Multiple buttons where only one can be active:

```tsx title="ToggleGroup.tsx" showLineNumbers
import { BooleanControl } from "@cutoff/audio-ui-react";
import { useState } from "react";

export default function ToggleGroupExample() {
  const [active, setActive] = useState<string | null>(null);

  const options = ["Option 1", "Option 2", "Option 3"];

  return (
    <div style={{ display: "flex", gap: "10px" }}>
      {options.map((option) => (
        <BooleanControl
          key={option}
          view={ToggleButtonView}
          value={active === option}
          onChange={(e) => {
            if (e.value) {
              setActive(option);
            } else {
              setActive(null);
            }
          }}
          latch={true}
          label={option}
        />
      ))}
    </div>
  );
}
```

### Momentary Action Button

Button that triggers an action but doesn't maintain state:

```tsx title="MomentaryAction.tsx" showLineNumbers
<BooleanControl
  view={ActionButtonView}
  value={false} // Always false
  onChange={(e) => {
    if (e.value) {
      handleAction();
    }
  }}
  latch={false} // Momentary
  label="Trigger"
/>
```

## Performance Considerations

* **Global event listeners**: Only attached during active drag sessions
* **Memoization**: Both BooleanControl and view components are memoized
* **Pointer tracking**: Efficient global pointer state management
* **Re-renders**: Only re-renders when value or props change

## Type Exports

```typescript
import {
  BooleanControl,
  BooleanControlComponentProps,
  ControlComponentView,
  ControlComponentViewProps,
} from "@cutoff/audio-ui-react";
```

## Examples

See the [View System and View Primitives](https://cutoff.dev/audio-ui/docs/latest/advanced-customization/view-system-view-primitives) guide for complete examples of building custom boolean controls with BooleanControl.

## Next Steps

* Learn about [ContinuousControl Primitive](https://cutoff.dev/audio-ui/docs/latest/advanced-customization/continuous-control-primitive) for continuous value controls
* Explore [DiscreteControl Primitive](https://cutoff.dev/audio-ui/docs/latest/advanced-customization/discrete-control-primitive) for discrete controls
* Check out [View Primitives](https://cutoff.dev/audio-ui/docs/latest/advanced-customization/view-system-view-primitives) for building custom visualizations


---

# Troubleshooting

# Troubleshooting

> Common issues and solutions when using AudioUI components.

## Control not displaying with adaptive sizing

If a control using `adaptiveSize={true}` (or the default adaptive behavior) does not appear, the parent container may have no dimension constraints. Adaptive sizing relies on container queries; without a bounded container, the control has nothing to size against.

**Solutions:**

* Use a fixed size when the component supports it (e.g. `adaptiveSize={false}` with `size="xlarge"` for Knob or Slider).
* Wrap the control in a container with explicit width and height so the control has a bounded area to size within.

## Knob value strip roundness is only square or round

The Knob's value strips (the arc indicating the current value) do not support a continuous roundness scale. Roundness is effectively on/off: `roundness={0.0}` gives a square linecap, and any non-zero value gives a round linecap. The correct behaviour has been achieved internally but does not pass our performance requirements, so the binary behaviour is used. If you need intermediate roundness on the knob ring outline, the prop still applies there; only the value strip uses the binary behaviour.
