{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "channel-strip",
  "title": "Channel Strip",
  "description": "A composable horizontal or console-style channel strip with shared audio config, status parts, responsive layout, and clipping reflection.",
  "dependencies": [
    "solid-js@^1.9.15",
    "class-variance-authority"
  ],
  "registryDependencies": [
    "@audiocn-solid/core",
    "@audiocn-solid/use-audio-config",
    "@audiocn-solid/badge"
  ],
  "files": [
    {
      "path": "components/ui/channel-strip.tsx",
      "content": "import { cva } from \"class-variance-authority\";\nimport type { VariantProps } from \"class-variance-authority\";\nimport {\n  createContext,\n  createSignal,\n  createUniqueId,\n  useContext,\n} from \"solid-js\";\n\nimport { Badge } from \"@/components/ui/badge\";\nimport type { BadgeProps } from \"@/components/ui/badge\";\nimport { AudioConfigProvider, useAudioConfig } from \"@/hooks/use-audio-config\";\nimport type { AudioSize } from \"@/hooks/use-audio-config\";\nimport type { Orientation } from \"@/lib/audio/types\";\nimport { provideContext } from \"@/lib/solid/context\";\nimport { createCompatEffect } from \"@/lib/solid/effect\";\nimport type { DivDOMProps, SpanDOMProps } from \"@/lib/solid/jsx-types\";\nimport { omitProps } from \"@/lib/solid/props\";\nimport { setRefValue } from \"@/lib/solid/ref\";\nimport type { RefTarget } from \"@/lib/solid/ref\";\nimport { mergeStyleVars } from \"@/lib/solid/style\";\nimport type { StyleValue } from \"@/lib/solid/style\";\nimport { cn } from \"@/lib/utils\";\n\ninterface ChannelStripContextValue {\n  readonly dimmed: boolean;\n  readonly muted: boolean;\n  readonly orientation: Orientation;\n  readonly solo: boolean;\n  readonly titleId: string;\n}\n\nconst ChannelStripContext = createContext<ChannelStripContextValue | null>(\n  null\n);\n\nconst useChannelStripContext = (part: string): ChannelStripContextValue => {\n  const context = useContext(ChannelStripContext);\n\n  if (!context) {\n    throw new Error(`${part} must be used inside ChannelStrip.`);\n  }\n\n  return context;\n};\n\n/** The state of the surrounding channel strip, for custom parts. */\nexport const useChannelStrip = (): ChannelStripContextValue =>\n  useChannelStripContext(\"useChannelStrip\");\n\nconst channelStripVariants = cva(\n  \"group/channel-strip data-selected:ring-ring/40 relative min-w-0 transition-[opacity,box-shadow] outline-none data-disabled:opacity-60 data-selected:ring-2\",\n  {\n    defaultVariants: {\n      orientation: \"horizontal\",\n      variant: \"default\",\n    },\n    variants: {\n      orientation: {\n        horizontal: \"@container/channel-strip w-full\",\n        vertical:\n          \"flex h-full min-h-72 w-[var(--channel-strip-width,6.5rem)] shrink-0 flex-col\",\n      },\n      variant: {\n        card: \"bg-card text-card-foreground rounded-xl border p-3 shadow-xs\",\n        default: \"bg-muted/40 rounded-xl p-3\",\n        ghost: \"p-2\",\n        master: \"bg-muted/60 ring-primary/10 rounded-xl border p-3 ring-1\",\n      },\n    },\n  }\n);\n\nconst channelStripLayoutVariants = cva(\"grid gap-x-3 gap-y-1.5\", {\n  defaultVariants: {\n    orientation: \"horizontal\",\n  },\n  variants: {\n    orientation: {\n      horizontal: [\n        \"w-full grid-cols-[minmax(0,1fr)_auto_auto] items-center [grid-template-areas:'header_header_header'_'meter_value_controls']\",\n        \"has-[>[data-slot=channel-strip-notice]]:[grid-template-areas:'header_header_header'_'meter_value_controls'_'notice_notice_notice']\",\n        \"has-[>[data-slot=channel-strip-fader]]:[grid-template-areas:'header_header_header'_'meter_value_controls'_'fader_value_controls']\",\n        \"has-[>[data-slot=channel-strip-fader]]:has-[>[data-slot=channel-strip-notice]]:[grid-template-areas:'header_header_header'_'meter_value_controls'_'fader_value_controls'_'notice_notice_notice']\",\n        \"@xl/channel-strip:grid-cols-[minmax(0,var(--channel-strip-header-width,12rem))_minmax(0,1fr)_auto_auto] @xl/channel-strip:[grid-template-areas:'header_meter_value_controls']\",\n        \"@xl/channel-strip:has-[>[data-slot=channel-strip-notice]]:[grid-template-areas:'header_meter_value_controls'_'notice_notice_notice_notice']\",\n        \"@xl/channel-strip:has-[>[data-slot=channel-strip-fader]]:[grid-template-areas:'header_meter_value_controls'_'header_fader_value_controls']\",\n        \"@xl/channel-strip:has-[>[data-slot=channel-strip-fader]]:has-[>[data-slot=channel-strip-notice]]:[grid-template-areas:'header_meter_value_controls'_'header_fader_value_controls'_'notice_notice_notice_notice']\",\n      ],\n      vertical:\n        \"flex-1 grid-cols-[1fr_auto_auto_1fr] grid-rows-[auto_minmax(0,1fr)_auto_auto_auto] justify-items-center [grid-template-areas:'header_header_header_header'_'._meter_fader_.'_'value_value_value_value'_'controls_controls_controls_controls'_'notice_notice_notice_notice']\",\n    },\n  },\n});\n\nexport interface ChannelStripProps\n  extends\n    Omit<DivDOMProps, \"children\" | \"class\" | \"className\" | \"ref\" | \"style\">,\n    Omit<VariantProps<typeof channelStripVariants>, \"orientation\"> {\n  accent?: string;\n  children?: DivDOMProps[\"children\"];\n  class?: string;\n  className?: string;\n  dimmed?: boolean;\n  disabled?: boolean;\n  muted?: boolean;\n  orientation?: Orientation;\n  ref?: RefTarget<HTMLDivElement>;\n  selected?: boolean;\n  size?: AudioSize;\n  solo?: boolean;\n  style?: StyleValue;\n}\n\nconst STRIP_OWN = [\n  \"accent\",\n  \"children\",\n  \"class\",\n  \"className\",\n  \"dimmed\",\n  \"disabled\",\n  \"muted\",\n  \"orientation\",\n  \"ref\",\n  \"selected\",\n  \"size\",\n  \"solo\",\n  \"style\",\n  \"variant\",\n] as const;\n\nexport const ChannelStrip = (props: ChannelStripProps) => {\n  const rest = omitProps(props, STRIP_OWN);\n\n  const config = useAudioConfig();\n\n  const orientation = () =>\n    props.orientation ?? config.orientation ?? \"horizontal\";\n\n  const size = () => props.size ?? config.size ?? \"default\";\n\n  const disabled = () => props.disabled ?? config.disabled ?? false;\n\n  const muted = () => props.muted ?? false;\n\n  const solo = () => props.solo ?? false;\n\n  const dimmed = () => props.dimmed ?? false;\n\n  const selected = () => props.selected ?? false;\n\n  const variant = () => props.variant ?? \"default\";\n\n  const titleId = `channel-strip-${createUniqueId()}-title`;\n\n  const [rootElement, setRootElement] = createSignal<HTMLDivElement | null>(\n    null\n  );\n\n  createCompatEffect(rootElement, (root) => {\n    if (!root || typeof MutationObserver === \"undefined\") {\n      return;\n    }\n\n    const update = () => {\n      const clipping =\n        root.querySelector(\"[data-slot='level-meter'][data-clipping]\") !== null;\n\n      root.toggleAttribute(\"data-clipping\", clipping);\n    };\n\n    const observer = new MutationObserver(update);\n\n    observer.observe(root, {\n      attributeFilter: [\"data-clipping\"],\n      attributes: true,\n      childList: true,\n      subtree: true,\n    });\n\n    update();\n\n    return () => {\n      observer.disconnect();\n    };\n  });\n\n  const context: ChannelStripContextValue = {\n    get dimmed() {\n      return dimmed();\n    },\n    get muted() {\n      return muted();\n    },\n    get orientation() {\n      return orientation();\n    },\n    get solo() {\n      return solo();\n    },\n    titleId,\n  };\n\n  const childConfig = {\n    get dimmed() {\n      return muted() || dimmed();\n    },\n    get disabled() {\n      return disabled();\n    },\n    get orientation() {\n      return orientation();\n    },\n    get size() {\n      return size();\n    },\n  };\n\n  return provideContext(ChannelStripContext, context, () => (\n    <div\n      aria-labelledby={titleId}\n      class={cn(\n        channelStripVariants({\n          orientation: orientation(),\n          variant: variant(),\n        }),\n        props.accent\n          ? orientation() === \"horizontal\"\n            ? \"before:absolute before:inset-y-3 before:left-0 before:w-0.5 before:rounded-full before:bg-(--channel-accent)\"\n            : \"before:absolute before:inset-x-3 before:top-0 before:h-0.5 before:rounded-full before:bg-(--channel-accent)\"\n          : undefined,\n        props.class,\n        props.className\n      )}\n      data-dimmed={dimmed() ? \"\" : undefined}\n      data-disabled={disabled() ? \"\" : undefined}\n      data-muted={muted() ? \"\" : undefined}\n      data-orientation={orientation()}\n      data-selected={selected() ? \"\" : undefined}\n      data-size={size()}\n      data-slot=\"channel-strip\"\n      data-solo={solo() ? \"\" : undefined}\n      data-variant={variant()}\n      ref={(node) => {\n        setRootElement(node);\n        setRefValue(props.ref, node);\n      }}\n      role=\"group\"\n      style={mergeStyleVars(\n        props.style,\n        props.accent\n          ? {\n              \"--channel-accent\": props.accent,\n            }\n          : {}\n      )}\n      {...rest}\n    >\n      <AudioConfigProvider value={childConfig}>\n        <div\n          class={channelStripLayoutVariants({\n            orientation: orientation(),\n          })}\n          data-slot=\"channel-strip-layout\"\n        >\n          {props.children}\n        </div>\n      </AudioConfigProvider>\n    </div>\n  ));\n};\n\ntype DivPartProps = Omit<DivDOMProps, \"class\" | \"className\"> & {\n  class?: string;\n  className?: string;\n};\n\ntype SpanPartProps = Omit<SpanDOMProps, \"class\" | \"className\"> & {\n  class?: string;\n  className?: string;\n};\n\nconst CLASS_OWN = [\"class\", \"className\"] as const;\n\nexport const ChannelStripHeader = (props: DivPartProps) => {\n  const context = useChannelStripContext(\"ChannelStripHeader\");\n\n  const rest = omitProps(props, CLASS_OWN);\n\n  return (\n    <div\n      class={cn(\n        \"flex min-w-0 items-center gap-2 [grid-area:header]\",\n        context.orientation === \"vertical\"\n          ? \"w-full flex-col text-center *:max-w-full\"\n          : \"flex-wrap\",\n        props.class,\n        props.className\n      )}\n      data-slot=\"channel-strip-header\"\n      {...rest}\n    />\n  );\n};\n\nexport const ChannelStripIcon = (props: SpanPartProps) => {\n  const rest = omitProps(props, CLASS_OWN);\n\n  return (\n    <span\n      aria-hidden=\"true\"\n      class={cn(\n        \"bg-background text-muted-foreground flex size-8 shrink-0 items-center justify-center rounded-lg shadow-xs [&_svg:not([class*='size-'])]:size-4\",\n        props.class,\n        props.className\n      )}\n      data-slot=\"channel-strip-icon\"\n      {...rest}\n    />\n  );\n};\n\nexport const ChannelStripTitle = (props: SpanPartProps) => {\n  const context = useChannelStripContext(\"ChannelStripTitle\");\n\n  const rest = omitProps(props, CLASS_OWN);\n\n  return (\n    <span\n      class={cn(\n        \"truncate text-sm leading-tight font-medium\",\n        props.class,\n        props.className\n      )}\n      data-slot=\"channel-strip-title\"\n      id={context.titleId}\n      {...rest}\n    />\n  );\n};\n\nexport const ChannelStripDescription = (props: SpanPartProps) => {\n  const rest = omitProps(props, CLASS_OWN);\n\n  return (\n    <span\n      class={cn(\n        \"text-muted-foreground truncate text-xs leading-tight\",\n        props.class,\n        props.className\n      )}\n      data-slot=\"channel-strip-description\"\n      {...rest}\n    />\n  );\n};\n\nexport const ChannelStripText = (props: DivPartProps) => {\n  const rest = omitProps(props, CLASS_OWN);\n\n  return (\n    <div\n      class={cn(\n        \"flex min-w-0 flex-1 flex-col gap-0.5 group-data-[orientation=horizontal]/channel-strip:min-w-20\",\n        props.class,\n        props.className\n      )}\n      data-slot=\"channel-strip-text\"\n      {...rest}\n    />\n  );\n};\n\nconst STATUS_CLASS = {\n  default: \"\",\n  error: \"\",\n  live: \"bg-meter-ok/15 text-meter-ok-foreground\",\n  muted: \"bg-channel-mute/15 text-channel-mute-foreground\",\n  warning: \"bg-meter-warn/20 text-foreground\",\n} as const;\n\nexport interface ChannelStripStatusProps extends BadgeProps {\n  tone?: \"default\" | \"live\" | \"muted\" | \"warning\" | \"error\";\n}\n\nconst STATUS_OWN = [\"tone\"] as const;\n\nexport const ChannelStripStatus = (props: ChannelStripStatusProps) => {\n  const rest = omitProps(props, STATUS_OWN);\n\n  const tone = () => props.tone ?? \"default\";\n\n  const badgeVariant = () => {\n    if (tone() === \"error\") {\n      return \"destructive\" as const;\n    }\n\n    if (tone() === \"default\") {\n      return \"secondary\" as const;\n    }\n\n    return \"outline\" as const;\n  };\n\n  return (\n    <Badge\n      {...rest}\n      class={cn(\"shrink-0\", STATUS_CLASS[tone()], props.class, props.className)}\n      data-slot=\"channel-strip-status\"\n      data-tone={tone()}\n      variant={badgeVariant()}\n    />\n  );\n};\n\nexport const ChannelStripActions = (props: DivPartProps) => {\n  const rest = omitProps(props, CLASS_OWN);\n\n  return (\n    <div\n      class={cn(\n        \"ml-auto flex shrink-0 items-center gap-1\",\n        props.class,\n        props.className\n      )}\n      data-slot=\"channel-strip-actions\"\n      {...rest}\n    />\n  );\n};\n\nexport const ChannelStripMeter = (props: DivPartProps) => {\n  const context = useChannelStripContext(\"ChannelStripMeter\");\n\n  const rest = omitProps(props, CLASS_OWN);\n\n  return (\n    <div\n      class={cn(\n        \"flex min-h-0 min-w-0 [grid-area:meter]\",\n        context.orientation === \"horizontal\"\n          ? \"w-full items-center\"\n          : \"h-full justify-center\",\n        props.class,\n        props.className\n      )}\n      data-slot=\"channel-strip-meter\"\n      {...rest}\n    />\n  );\n};\n\nexport const ChannelStripFader = (props: DivPartProps) => {\n  const context = useChannelStripContext(\"ChannelStripFader\");\n\n  const rest = omitProps(props, CLASS_OWN);\n\n  return (\n    <div\n      class={cn(\n        \"flex min-h-0 min-w-0 [grid-area:fader] [&_[data-slot=fader-control]]:p-0 [&_[data-slot=fader-scale]]:p-0\",\n        context.orientation === \"horizontal\"\n          ? \"w-full items-center [&_[data-slot=fader-control]:only-child]:my-[calc((var(--fader-track-size)-var(--fader-thumb-size))/2)]\"\n          : \"h-full justify-center [&_[data-slot=fader-control]:only-child]:mx-[calc((var(--fader-track-size)-var(--fader-thumb-size))/2)]\",\n        props.class,\n        props.className\n      )}\n      data-slot=\"channel-strip-fader\"\n      {...rest}\n    />\n  );\n};\n\nexport const ChannelStripValue = (props: DivPartProps) => {\n  const rest = omitProps(props, CLASS_OWN);\n\n  return (\n    <div\n      class={cn(\n        \"text-muted-foreground flex min-w-[8ch] items-center justify-end font-mono text-xs whitespace-nowrap tabular-nums [grid-area:value] group-data-[orientation=vertical]/channel-strip:justify-center\",\n        props.class,\n        props.className\n      )}\n      data-slot=\"channel-strip-value\"\n      {...rest}\n    />\n  );\n};\n\nexport const ChannelStripControls = (props: DivPartProps) => {\n  const rest = omitProps(props, CLASS_OWN);\n\n  return (\n    <div\n      class={cn(\n        \"flex items-center justify-center gap-1 [grid-area:controls]\",\n        props.class,\n        props.className\n      )}\n      data-slot=\"channel-strip-controls\"\n      {...rest}\n    />\n  );\n};\n\nconst noticeVariants = cva(\n  \"flex min-w-0 items-center gap-2 rounded-lg px-2.5 py-1.5 text-xs [grid-area:notice] [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5\",\n  {\n    defaultVariants: {\n      variant: \"default\",\n    },\n    variants: {\n      variant: {\n        default: \"bg-muted text-muted-foreground\",\n        destructive: \"bg-destructive/10 text-destructive\",\n        warning: \"bg-meter-warn/15 text-foreground\",\n      },\n    },\n  }\n);\n\nexport interface ChannelStripNoticeProps\n  extends DivPartProps, VariantProps<typeof noticeVariants> {}\n\nconst NOTICE_OWN = [\"class\", \"className\", \"variant\"] as const;\n\nexport const ChannelStripNotice = (props: ChannelStripNoticeProps) => {\n  const rest = omitProps(props, NOTICE_OWN);\n\n  const variant = () => props.variant ?? \"default\";\n\n  return (\n    <div\n      class={cn(\n        noticeVariants({\n          variant: variant(),\n        }),\n        \"w-full\",\n        props.class,\n        props.className\n      )}\n      data-slot=\"channel-strip-notice\"\n      data-variant={variant()}\n      role={variant() === \"destructive\" ? \"alert\" : \"status\"}\n      {...rest}\n    />\n  );\n};\n\nexport { channelStripLayoutVariants, channelStripVariants };\n",
      "type": "registry:ui"
    },
    {
      "path": "lib/audiocn-license.txt",
      "content": "Audio component source adapted from https://github.com/audiocn/ui.\nMIT License — Copyright (c) 2026 OrcDev\n\nRegular SVG icon geometry adapted from @phosphor-icons/react 2.1.10.\nMIT License — Copyright (c) 2020 Phosphor Icons\n\nThe following permission and warranty terms apply to both notices.\nRetain this file with copies or substantial portions of the source.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n",
      "type": "registry:file",
      "target": "@lib/audiocn-license.txt"
    }
  ],
  "type": "registry:ui"
}
