{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "channel-strip",
  "type": "registry:component",
  "title": "Channel strip",
  "description": "Edits channel gain, pan, mute, and solo with native sliders and 44px toggle actions wired to caller-owned state.",
  "registryDependencies": [
    "https://vlak.dev/r/vlak-base.json",
    "https://vlak.dev/r/inter.json",
    "https://vlak.dev/r/button.json",
    "https://vlak.dev/r/vlak-lib.json"
  ],
  "dependencies": [
    "@stylexjs/stylex"
  ],
  "devDependencies": [
    "@stylexjs/babel-plugin"
  ],
  "docs": "Vlak leaves are StyleX. Compile them with @stylexjs/babel-plugin (Vite: @stylexjs/unplugin, Next: @stylexjs/nextjs-plugin). If you would rather not run a compiler, import @noorddev/vlak-react instead: it ships precompiled with one stylesheet.",
  "files": [
    {
      "path": "vlak/channel-strip.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as stylex from \"@stylexjs/stylex\";\nimport { vlak, mq } from \"./tokens.stylex\";\nimport { rs } from \"./rs\";\nimport { useInputValue } from \"./use-input-value\";\nimport { useMergedRefs } from \"./merge-refs\";\nimport { Button } from \"./button\";\n\nexport interface ChannelStripValue { gain: number; pan: number; muted: boolean; solo: boolean }\nexport interface ChannelStripProps extends Omit<React.FieldsetHTMLAttributes<HTMLFieldSetElement>, \"onChange\" | \"defaultValue\"> {\n  label: React.ReactNode;\n  value?: ChannelStripValue;\n  defaultValue?: ChannelStripValue;\n  onValueChange?: (value: ChannelStripValue) => void;\n  gainMin?: number;\n  gainMax?: number;\n  readOnly?: boolean;\n}\n\nconst initial: ChannelStripValue = { gain: 0, pan: 0, muted: false, solo: false };\nconst styles = stylex.create({\n  root: { display: \"grid\", gap: \"0.75rem\", minWidth: 0, borderWidth: vlak.hairline, borderStyle: \"solid\", borderColor: vlak.divider, padding: \"1rem\", margin: 0, color: vlak.ink },\n  label: { paddingInline: \"0.25rem\", fontSize: vlak.controlFs, lineHeight: 1.45, fontWeight: 600 },\n  field: { display: \"grid\", gap: \"0.25rem\", fontSize: vlak.controlLabel, lineHeight: 1.45 },\n  text: { display: \"flex\", flexWrap: \"wrap\", justifyContent: \"space-between\", gap: \"0.5rem\", fontVariantNumeric: \"tabular-nums\" },\n  range: { display: \"block\", margin: 0, width: \"100%\", minWidth: vlak.hit, minHeight: vlak.hit, accentColor: vlak.ink, outlineWidth: { default: null, \":focus-visible\": 2 }, outlineStyle: { default: null, \":focus-visible\": \"solid\" }, outlineColor: vlak.ink, outlineOffset: 2 },\n  actions: { display: \"flex\", flexWrap: \"wrap\", gap: \"0.5rem\" },\n  button: { minWidth: vlak.hit, minHeight: vlak.hit, width: \"auto\", maxWidth: \"100%\" },\n  pressed: { backgroundColor: { default: vlak.ink, [mq.forcedColors]: \"Highlight\" }, color: { default: vlak.paper, [mq.forcedColors]: \"HighlightText\" } },\n});\n\n/** Gain, pan, mute and solo state for an application-owned audio channel. */\nexport const ChannelStrip = React.forwardRef<HTMLFieldSetElement, ChannelStripProps>(function ChannelStrip({ label, value, defaultValue = initial, onValueChange, gainMin = -60, gainMax = 12, readOnly, name, form, disabled, className, style, ...props }, ref) {\n  const [, refresh] = React.useReducer((n: number) => n + 1, 0);\n  const [current, setValue, fieldRef] = useInputValue<ChannelStripValue, HTMLFieldSetElement>(value, defaultValue, onValueChange, refresh);\n  const mergedRef = useMergedRefs(ref, fieldRef);\n  const root = rs([\"rs-channel-strip\", className], styles.root);\n  const heading = rs([\"rs-channel-strip-label\"], styles.label);\n  const field = rs([\"rs-channel-strip-field\"], styles.field);\n  const text = rs([\"rs-channel-strip-text\"], styles.text);\n  const range = rs([\"rs-channel-strip-range\"], styles.range);\n  const actions = rs([\"rs-channel-strip-actions\"], styles.actions);\n  const gainBoundsValid = Number.isFinite(gainMin) && Number.isFinite(gainMax) && Number.isFinite(gainMax - gainMin) && gainMax > gainMin;\n  const gainValid = gainBoundsValid && Number.isFinite(current.gain) && current.gain >= gainMin && current.gain <= gainMax;\n  const panValid = Number.isFinite(current.pan) && current.pan >= -100 && current.pan <= 100;\n  const gainText = !gainBoundsValid ? \"Range unavailable\" : !Number.isFinite(current.gain) ? \"Unavailable\" : `${current.gain} dB${gainValid ? \"\" : \" · Outside range\"}`;\n  const panText = !Number.isFinite(current.pan) ? \"Unavailable\" : `${current.pan === 0 ? \"Center\" : `${Math.abs(current.pan)}% ${current.pan < 0 ? \"left\" : \"right\"}`}${panValid ? \"\" : \" · Outside range\"}`;\n  return <fieldset {...props} ref={mergedRef} name={name} form={form} disabled={disabled} className={root.className} style={{ ...root.style, ...style }}>\n    <legend className={heading.className} style={heading.style}>{label}</legend>\n    <label className={field.className} style={field.style}><span className={text.className} style={text.style}><span>Gain</span><span>{gainText}</span></span><input type=\"range\" aria-label=\"Gain\" form={form} name={name ? `${name}.gain` : undefined} min={gainValid ? gainMin : 0} max={gainValid ? gainMax : 1} step=\"any\" value={gainValid ? current.gain : 0} disabled={disabled || readOnly || !gainValid} aria-invalid={!gainValid || undefined} aria-valuetext={gainText} className={range.className} style={range.style} onChange={(event) => setValue({ ...current, gain: Number(event.currentTarget.value) })} /></label>\n    <label className={field.className} style={field.style}><span className={text.className} style={text.style}><span>Pan</span><span>{panText}</span></span><input type=\"range\" aria-label=\"Pan\" form={form} name={name ? `${name}.pan` : undefined} min={-100} max={100} step=\"any\" value={panValid ? current.pan : 0} disabled={disabled || readOnly || !panValid} aria-invalid={!panValid || undefined} aria-valuetext={panText} className={range.className} style={range.style} onChange={(event) => setValue({ ...current, pan: Number(event.currentTarget.value) })} /></label>\n    <div className={actions.className} style={actions.style}>{([['muted', 'Mute'], ['solo', 'Solo']] as const).map(([key, title]) => {\n      const button = rs([\"rs-channel-strip-button\", current[key] && \"rs-channel-strip-pressed\"], styles.button, current[key] && styles.pressed);\n      return <Button key={key} variant={current[key] ? \"primary\" : \"ghost\"} size=\"sm\" disabled={disabled || readOnly} aria-pressed={current[key]} {...button} onClick={() => setValue({ ...current, [key]: !current[key] })}>{title}</Button>;\n    })}</div>\n    {name && <><input type=\"hidden\" name={`${name}.muted`} form={form} value={String(current.muted)} /><input type=\"hidden\" name={`${name}.solo`} form={form} value={String(current.solo)} /></>}\n    {readOnly && name && <>{gainValid && <input type=\"hidden\" name={`${name}.gain`} form={form} value={current.gain} />}{panValid && <input type=\"hidden\" name={`${name}.pan`} form={form} value={current.pan} />}</>}\n  </fieldset>;\n});\n",
      "type": "registry:component",
      "target": "components/vlak/channel-strip.tsx"
    },
    {
      "path": "vlak/use-input-value.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\n/** Shared state for compound inputs. Native form reset restores uncontrolled defaults. */\nexport function useInputValue<T, E extends HTMLElement>(\n  value: T | undefined,\n  defaultValue: T,\n  onValueChange?: (value: T) => void,\n  onReset?: () => void,\n) {\n  const [inner, setInner] = React.useState(defaultValue);\n  const element = React.useRef<E>(null);\n  const controlled = value !== undefined;\n  const reset = React.useRef({ controlled, defaultValue, onReset });\n  reset.current = { controlled, defaultValue, onReset };\n  React.useEffect(() => {\n    const node = element.current;\n    const form = node && \"form\" in node ? (node as unknown as HTMLInputElement).form : node?.closest(\"form\");\n    if (!form) return;\n    const handleReset = (event: Event) => {\n      queueMicrotask(() => {\n        if (!event.defaultPrevented) {\n          if (!reset.current.controlled) setInner(reset.current.defaultValue);\n          reset.current.onReset?.();\n        }\n      });\n    };\n    form.addEventListener(\"reset\", handleReset);\n    return () => form.removeEventListener(\"reset\", handleReset);\n  }, []);\n  const setValue = (next: T) => {\n    if (!controlled) setInner(next);\n    onValueChange?.(next);\n  };\n  return [controlled ? value : inner, setValue, element] as const;\n}\n",
      "type": "registry:file",
      "target": "components/vlak/use-input-value.ts"
    },
    {
      "path": "vlak/styles/channel-strip.css",
      "content": "/* ── channel-strip: generated from packages/react/src/components/channel-strip.tsx ── */\n.rs-channel-strip{display:grid;gap:0.75rem;min-width:0;border-width:1px;border-style:solid;border-color:var(--divider);padding:1rem;margin:0;color:var(--text)}\n.rs-channel-strip-label{padding-inline:0.25rem;font-size:var(--control-fs);line-height:1.45;font-weight:600}\n.rs-channel-strip-field{display:grid;gap:0.25rem;font-size:var(--control-label);line-height:1.45}\n.rs-channel-strip-text{display:flex;flex-wrap:wrap;justify-content:space-between;gap:0.5rem;font-variant-numeric:tabular-nums}\n.rs-channel-strip-range{display:block;margin:0;width:100%;min-width:var(--hit);min-height:var(--hit);accent-color:var(--text);outline-color:var(--text);outline-offset:2px}\n.rs-channel-strip-range:focus-visible{outline-width:2px;outline-style:solid}\n.rs-channel-strip-actions{display:flex;flex-wrap:wrap;gap:0.5rem}\n.rs-channel-strip-button{min-width:var(--hit);min-height:var(--hit);width:auto;max-width:100%}\n.rs-channel-strip-pressed{background-color:var(--text);color:var(--bg)}\n@media (forced-colors: active){.rs-channel-strip-pressed{background-color:Highlight;color:HighlightText}}\n",
      "type": "registry:file",
      "target": "styles/vlak/channel-strip.css"
    }
  ],
  "meta": {
    "vlak": {
      "category": "creative",
      "classes": [
        "rs-channel-strip",
        "rs-channel-strip-label",
        "rs-channel-strip-field",
        "rs-channel-strip-text",
        "rs-channel-strip-range",
        "rs-channel-strip-actions",
        "rs-channel-strip-button",
        "rs-channel-strip-pressed"
      ],
      "snippet": "<fieldset class=\"rs-channel-strip\"><legend class=\"rs-channel-strip-label\">Dialogue</legend><label class=\"rs-channel-strip-field\">Gain<input class=\"rs-channel-strip-range\" type=\"range\" min=\"-60\" max=\"12\" step=\"any\" value=\"0\" /></label><label class=\"rs-channel-strip-field\">Pan<input class=\"rs-channel-strip-range\" type=\"range\" min=\"-100\" max=\"100\" step=\"any\" value=\"0\" /></label><div class=\"rs-channel-strip-actions\"><button class=\"rs-btn-ghost rs-btn-sm rs-channel-strip-button\" type=\"button\" aria-pressed=\"false\">Mute</button><button class=\"rs-btn-ghost rs-btn-sm rs-channel-strip-button\" type=\"button\" aria-pressed=\"false\">Solo</button></div></fieldset>",
      "cssOnly": false,
      "registryDependencies": [
        "button",
        "vlak-lib"
      ],
      "aliases": [
        "ChannelStrip",
        "Mixer channel",
        "Audio mixer",
        "Gain control",
        "Pan control",
        "Mute solo"
      ],
      "example": "import { ChannelStrip } from \"@noorddev/vlak-react\";\n\n<ChannelStrip label=\"Dialogue\" name=\"dialogue\" defaultValue={{ gain: -3, pan: 0, muted: false, solo: false }} />",
      "usage": {
        "use": [
          "A host channel whose gain, pan, mute, and solo states belong together.",
          "value/defaultValue/onValueChange for controlled or local editing; connect changes to your audio host.",
          "Pan from -100 percent left through zero center to 100 percent right; gain uses decibels."
        ],
        "avoid": [
          "Assuming UI changes process audio without a connected host.",
          "Using solo as a global mixing policy; the application coordinates other channels."
        ]
      },
      "keyboard": [
        {
          "keys": "Tab",
          "does": "Moves through enabled gain, pan, mute, and solo controls."
        },
        {
          "keys": "Arrow keys, Home, End",
          "does": "Uses native slider stepping and bounds."
        },
        {
          "keys": "Enter, Space",
          "does": "Toggles the focused mute or solo action."
        }
      ],
      "a11y": [
        "A fieldset and legend name the channel; native sliders expose gain and pan bounds and descriptive value text. In-range values retain their supplied precision.",
        "Vlak Button supplies mute and solo actions with stable names and aria-pressed; their entire surface changes on selection.",
        "All controls meet the 44px target size. Disabled or read-only controls cannot edit the channel.",
        "Uncontrolled form reset restores the supplied default; named read-only channels retain all valid values in form submissions. Disabled channels and out-of-range sliders are omitted. The ref reaches the fieldset."
      ]
    }
  }
}
