{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "channel-strip",
  "title": "Channel Strip",
  "description": "One channel of a mixer: header, status, meter, fader, value, controls and a notice, as a row or a console strip. The master strip is a variant.",
  "dependencies": [
    "@base-ui/react",
    "class-variance-authority"
  ],
  "registryDependencies": [
    "badge",
    "@audiocn/use-audio-config"
  ],
  "files": [
    {
      "path": "components/ui/channel-strip.tsx",
      "content": "\"use client\";\n\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport { cva } from \"class-variance-authority\";\nimport type { VariantProps } from \"class-variance-authority\";\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n} from \"react\";\nimport type { ComponentProps, CSSProperties } from \"react\";\n\nimport { Badge } 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 { cn } from \"@/lib/utils\";\n\ninterface ChannelStripContextValue {\n  orientation: Orientation;\n  titleId: string;\n  muted: boolean;\n  solo: boolean;\n  dimmed: boolean;\n}\n\nconst ChannelStripContext = createContext<ChannelStripContextValue | null>(\n  null\n);\n\nconst useChannelStripContext = (part: string) => {\n  const context = useContext(ChannelStripContext);\n  if (!context) {\n    throw new Error(`${part} must be used inside ChannelStrip.`);\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        // A row measures itself, so it can stack its header on narrow widths.\n        horizontal: \"@container/channel-strip w-full\",\n        // Console strips keep their width and let the mixer scroll instead.\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\n/**\n * The grid the parts place themselves on, by area name. A row stacks its\n * header above the meter and fader until the strip is 36rem wide. The fader\n * row exists only when a fader does: an empty one would leave the meter in\n * the top half of the header and the value beside it.\n */\nconst channelStripLayoutVariants = cva(\"grid gap-x-3 gap-y-1.5\", {\n  defaultVariants: { orientation: \"horizontal\" },\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    useRender.ComponentProps<\"div\">,\n    Omit<VariantProps<typeof channelStripVariants>, \"orientation\"> {\n  /** Horizontal is a row; vertical is a console strip. Inherited from a mixer. */\n  orientation?: Orientation;\n  size?: AudioSize;\n  muted?: boolean;\n  solo?: boolean;\n  /** Silenced by another channel's solo. */\n  dimmed?: boolean;\n  selected?: boolean;\n  /** Disables every control inside. */\n  disabled?: boolean;\n  /** A CSS colour for the channel's colour tag. */\n  accent?: string;\n}\n\nexport const ChannelStrip = ({\n  orientation: orientationProp,\n  variant = \"default\",\n  size: sizeProp,\n  muted = false,\n  solo = false,\n  dimmed = false,\n  selected = false,\n  disabled: disabledProp,\n  accent,\n  render,\n  className,\n  style,\n  children,\n  ref,\n  ...props\n}: ChannelStripProps) => {\n  const config = useAudioConfig();\n  const orientation = orientationProp ?? config.orientation ?? \"horizontal\";\n  const size = sizeProp ?? config.size ?? \"default\";\n  const disabled = disabledProp ?? config.disabled ?? false;\n  const titleId = useId();\n  const rootRef = useRef<HTMLElement | null>(null);\n\n  useEffect(() => {\n    const root = rootRef.current;\n    if (!root || typeof MutationObserver === \"undefined\") {\n      return;\n    }\n    const update = () => {\n      const clipping =\n        root.querySelector(\"[data-slot='level-meter'][data-clipping]\") !== null;\n      root.toggleAttribute(\"data-clipping\", clipping);\n    };\n    const observer = new MutationObserver(update);\n    // childList too: a meter removed mid-clip must clear the strip.\n    observer.observe(root, {\n      attributeFilter: [\"data-clipping\"],\n      attributes: true,\n      childList: true,\n      subtree: true,\n    });\n    update();\n    return () => {\n      observer.disconnect();\n    };\n  }, []);\n\n  const setRootRef = useCallback(\n    (node: HTMLElement | null) => {\n      rootRef.current = node;\n      if (typeof ref === \"function\") {\n        ref(node as HTMLDivElement | null);\n      } else if (ref) {\n        ref.current = node as HTMLDivElement | null;\n      }\n    },\n    [ref]\n  );\n\n  const contextValue = useMemo<ChannelStripContextValue>(\n    () => ({ dimmed, muted, orientation, solo, titleId }),\n    [dimmed, muted, orientation, solo, titleId]\n  );\n\n  const element = useRender({\n    defaultTagName: \"div\",\n    props: mergeProps<\"div\">(\n      {\n        \"aria-labelledby\": titleId,\n        children: (\n          <ChannelStripContext.Provider value={contextValue}>\n            <AudioConfigProvider\n              value={{ dimmed: muted || dimmed, disabled, orientation, size }}\n            >\n              <div\n                className={channelStripLayoutVariants({ orientation })}\n                data-slot=\"channel-strip-layout\"\n              >\n                {children}\n              </div>\n            </AudioConfigProvider>\n          </ChannelStripContext.Provider>\n        ),\n        className: cn(\n          channelStripVariants({ orientation, variant }),\n          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          className\n        ),\n        role: \"group\",\n        style: {\n          ...(accent ? { \"--channel-accent\": accent } : {}),\n          ...style,\n        } as CSSProperties,\n      },\n      props\n    ),\n    ref: setRootRef,\n    render,\n    state: {\n      dimmed,\n      disabled,\n      muted,\n      orientation,\n      selected,\n      size,\n      slot: \"channel-strip\",\n      solo,\n      variant,\n    },\n  });\n\n  return element;\n};\n\nexport const ChannelStripHeader = ({\n  className,\n  ...props\n}: ComponentProps<\"div\">) => {\n  const { orientation } = useChannelStripContext(\"ChannelStripHeader\");\n  return (\n    <div\n      className={cn(\n        \"flex min-w-0 items-center gap-2 [grid-area:header]\",\n        orientation === \"vertical\"\n          ? \"w-full flex-col text-center *:max-w-full\"\n          : \"flex-wrap\",\n        className\n      )}\n      data-slot=\"channel-strip-header\"\n      {...props}\n    />\n  );\n};\n\nexport const ChannelStripIcon = ({\n  className,\n  ...props\n}: ComponentProps<\"span\">) => (\n  <span\n    aria-hidden\n    className={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      className\n    )}\n    data-slot=\"channel-strip-icon\"\n    {...props}\n  />\n);\n\nexport const ChannelStripTitle = ({\n  className,\n  ...props\n}: ComponentProps<\"span\">) => {\n  const { titleId } = useChannelStripContext(\"ChannelStripTitle\");\n  return (\n    <span\n      className={cn(\"truncate text-sm leading-tight font-medium\", className)}\n      data-slot=\"channel-strip-title\"\n      id={titleId}\n      {...props}\n    />\n  );\n};\n\nexport const ChannelStripDescription = ({\n  className,\n  ...props\n}: ComponentProps<\"span\">) => (\n  <span\n    className={cn(\n      \"text-muted-foreground truncate text-xs leading-tight\",\n      className\n    )}\n    data-slot=\"channel-strip-description\"\n    {...props}\n  />\n);\n\n/** Groups a title and description so they stack next to an icon. */\nexport const ChannelStripText = ({\n  className,\n  ...props\n}: ComponentProps<\"div\">) => (\n  <div\n    className={cn(\n      \"flex min-w-0 flex-1 flex-col gap-0.5 group-data-[orientation=horizontal]/channel-strip:min-w-20\",\n      className\n    )}\n    data-slot=\"channel-strip-text\"\n    {...props}\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 ComponentProps<typeof Badge> {\n  tone?: \"default\" | \"live\" | \"muted\" | \"warning\" | \"error\";\n}\n\nexport const ChannelStripStatus = ({\n  tone = \"default\",\n  className,\n  ...props\n}: ChannelStripStatusProps) => {\n  let variant: \"secondary\" | \"destructive\" | \"outline\" = \"outline\";\n  if (tone === \"error\") {\n    variant = \"destructive\";\n  } else if (tone === \"default\") {\n    variant = \"secondary\";\n  }\n  return (\n    <Badge\n      className={cn(\"shrink-0\", STATUS_CLASS[tone], className)}\n      data-slot=\"channel-strip-status\"\n      data-tone={tone}\n      variant={variant}\n      {...props}\n    />\n  );\n};\n\nexport const ChannelStripActions = ({\n  className,\n  ...props\n}: ComponentProps<\"div\">) => (\n  <div\n    className={cn(\"ml-auto flex shrink-0 items-center gap-1\", className)}\n    data-slot=\"channel-strip-actions\"\n    {...props}\n  />\n);\n\nexport const ChannelStripMeter = ({\n  className,\n  ...props\n}: ComponentProps<\"div\">) => {\n  const { orientation } = useChannelStripContext(\"ChannelStripMeter\");\n  return (\n    <div\n      className={cn(\n        \"flex min-h-0 min-w-0 [grid-area:meter]\",\n        orientation === \"horizontal\"\n          ? \"w-full items-center\"\n          : \"h-full justify-center\",\n        className\n      )}\n      data-slot=\"channel-strip-meter\"\n      {...props}\n    />\n  );\n};\n\nexport const ChannelStripFader = ({\n  className,\n  ...props\n}: ComponentProps<\"div\">) => {\n  const { orientation } = useChannelStripContext(\"ChannelStripFader\");\n  return (\n    <div\n      className={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        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        className\n      )}\n      data-slot=\"channel-strip-fader\"\n      {...props}\n    />\n  );\n};\n\nexport const ChannelStripValue = ({\n  className,\n  ...props\n}: ComponentProps<\"div\">) => (\n  <div\n    className={cn(\n      // Wide enough for \"−60.0 dB\", so dragging a fader never resizes the row.\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      className\n    )}\n    data-slot=\"channel-strip-value\"\n    {...props}\n  />\n);\n\nexport const ChannelStripControls = ({\n  className,\n  ...props\n}: ComponentProps<\"div\">) => (\n  <div\n    className={cn(\n      \"flex items-center justify-center gap-1 [grid-area:controls]\",\n      className\n    )}\n    data-slot=\"channel-strip-controls\"\n    {...props}\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: { variant: \"default\" },\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 ComponentProps<\"div\">, VariantProps<typeof noticeVariants> {}\n\nexport const ChannelStripNotice = ({\n  variant = \"default\",\n  className,\n  ...props\n}: ChannelStripNoticeProps) => (\n  <div\n    className={cn(noticeVariants({ variant }), \"w-full\", className)}\n    data-slot=\"channel-strip-notice\"\n    data-variant={variant}\n    role={variant === \"destructive\" ? \"alert\" : \"status\"}\n    {...props}\n  />\n);\n\nexport { channelStripLayoutVariants, channelStripVariants };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}