import * as React from 'react'; import { KnobContextProvider, type KnobRenderContext } from '../core/context'; import { useKnobTheme } from '../core/theme'; import type { KnobCoreProps } from '../core/types'; import { useKnob } from '../hooks/useKnob'; import { ValueInput, defaultParseValue } from './ValueInput'; export interface KnobProps extends KnobCoreProps { /** Square canvas size in px. Default: 80. */ size?: number; /** * Color of the keyboard focus ring, or `false` to disable it (only do this * if you render your own focus indicator). Default: the theme's ring. */ focusRing?: string | false; /** * Double-click opens a type-in editor instead of resetting: type an exact * value, Enter/blur commits, Escape cancels. Accepts "1.2k" style input. */ editable?: boolean; /** Custom parser for typed input, e.g. to accept "-6 dB" or "A4". */ parseValue?: (text: string) => number | null; className?: string; style?: React.CSSProperties; /** SVG children (Arc, Pointer, Ticks, ...) or a render function receiving the live state. */ children?: React.ReactNode | ((ctx: KnobRenderContext) => React.ReactNode); /** Extra content rendered inside the wrapper but outside the SVG (e.g. HTML labels). */ overlay?: React.ReactNode | ((ctx: KnobRenderContext) => React.ReactNode); } /** * Headless-but-visual knob container: wires up the full interaction model * (rotary/relative/track drag, wheel, keyboard, double-click reset, a11y) * and provides a render context for composable SVG primitives. */ export const Knob: React.FC = ({ size = 80, focusRing, editable = false, parseValue, className, style, children, overlay, ...core }) => { const theme = useKnobTheme(); const ring = focusRing ?? theme.focusRing; const knob = useKnob(core); const [editing, setEditing] = React.useState(false); const closeEditor = () => { setEditing(false); knob.ref.current?.focus({ preventScroll: true }); }; const onDoubleClick = editable ? (e: React.MouseEvent) => { e.stopPropagation(); setEditing(true); } : knob.bind.onDoubleClick; const ctx: KnobRenderContext = { value: knob.value, normalized: knob.normalized, originNormalized: knob.originNormalized, angle: knob.angle, isDragging: knob.isDragging, min: knob.min, max: knob.max, decimals: knob.decimals, angleOffset: knob.angleOffset, angleRange: knob.angleRange, size, center: size / 2, setValue: knob.setValue, }; return (
{typeof overlay === 'function' ? overlay(ctx) : overlay} {editing && ( { const parsed = (parseValue ?? defaultParseValue)(raw); if (parsed !== null && Number.isFinite(parsed)) knob.setValue(parsed); closeEditor(); }} onCancel={closeEditor} /> )}
); };