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:
optionsprop: 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:
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:
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):
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):
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:
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:
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
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:
<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:
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:
<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)
<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:
// 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:
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:
<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:
<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:
<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:
<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:
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
// 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
import {
DiscreteControl,
DiscreteControlComponentProps,
OptionView,
ControlComponentView,
ControlComponentViewProps,
} from "@cutoff/audio-ui-react";Next Steps
- Learn about ContinuousControl Primitive for continuous value controls
- Explore BooleanControl Primitive for boolean controls
- Check out View Primitives for building custom visualizations