{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "mixer",
  "title": "Mixer",
  "description": "The container for channel strips: shared meter settings, keyboard navigation between strips, a master area and an empty state.",
  "registryDependencies": [
    "@audiocn/core",
    "@audiocn/use-audio-config",
    "@audiocn/channel-strip"
  ],
  "files": [
    {
      "path": "components/ui/mixer.tsx",
      "content": "\"use client\";\n\nimport { createContext, useContext, useId, useMemo } from \"react\";\nimport type { ComponentProps, KeyboardEvent } from \"react\";\n\nimport { AudioConfigProvider } from \"@/hooks/use-audio-config\";\nimport type { AudioSize } from \"@/hooks/use-audio-config\";\nimport type { BallisticsInput } from \"@/lib/audio/ballistics\";\nimport type { MeterZone, Orientation } from \"@/lib/audio/types\";\nimport { cn } from \"@/lib/utils\";\n\nconst FOCUSABLE = \"input, button, [tabindex]:not([tabindex='-1'])\";\nconst STRIP_SELECTOR = \"[data-slot='channel-strip']\";\n\ninterface MixerContextValue {\n  orientation: Orientation;\n  titleId: string;\n}\n\nconst MixerContext = createContext<MixerContextValue | null>(null);\n\nconst useMixerPart = (part: string) => {\n  const context = useContext(MixerContext);\n  if (!context) {\n    throw new Error(`${part} must be used inside Mixer.`);\n  }\n  return context;\n};\n\n/** The surrounding mixer's layout, for custom parts. */\nexport const useMixerContext = (): MixerContextValue =>\n  useMixerPart(\"useMixerContext\");\n\nexport interface MixerProps extends ComponentProps<\"div\"> {\n  /**\n   * `horizontal` stacks row strips; `vertical` lays console strips side by\n   * side. Default `horizontal`.\n   */\n  orientation?: Orientation;\n  size?: AudioSize;\n  /** Shared meter range. */\n  minDb?: number;\n  maxDb?: number;\n  /** Shared meter zones. */\n  zones?: MeterZone[];\n  /** Shared meter movement. */\n  ballistics?: BallisticsInput;\n  disabled?: boolean;\n}\n\nexport const Mixer = ({\n  orientation = \"horizontal\",\n  size,\n  minDb,\n  maxDb,\n  zones,\n  ballistics,\n  disabled,\n  className,\n  children,\n  ...props\n}: MixerProps) => {\n  const titleId = useId();\n  const contextValue = useMemo<MixerContextValue>(\n    () => ({ orientation, titleId }),\n    [orientation, titleId]\n  );\n  const stripOrientation: Orientation =\n    orientation === \"horizontal\" ? \"horizontal\" : \"vertical\";\n\n  return (\n    <MixerContext.Provider value={contextValue}>\n      <AudioConfigProvider\n        value={{\n          ballistics,\n          disabled,\n          maxDb,\n          minDb,\n          orientation: stripOrientation,\n          size,\n          zones,\n        }}\n      >\n        <div\n          aria-labelledby={titleId}\n          className={cn(\n            \"group/mixer grid min-w-0 gap-3 [--mixer-gap:0.5rem]\",\n            orientation === \"horizontal\"\n              ? \"grid-cols-1 [grid-template-areas:'header'_'channels'_'separator'_'master']\"\n              : \"grid-cols-[minmax(0,1fr)_auto_auto] grid-rows-[auto_minmax(0,1fr)] [grid-template-areas:'header_header_header'_'channels_separator_master']\",\n            className\n          )}\n          data-orientation={orientation}\n          data-size={size}\n          data-slot=\"mixer\"\n          role=\"group\"\n          {...props}\n        >\n          {children}\n        </div>\n      </AudioConfigProvider>\n    </MixerContext.Provider>\n  );\n};\n\nexport const MixerHeader = ({ className, ...props }: ComponentProps<\"div\">) => (\n  <div\n    className={cn(\n      \"flex min-w-0 items-center gap-2 [grid-area:header]\",\n      className\n    )}\n    data-slot=\"mixer-header\"\n    {...props}\n  />\n);\n\nexport const MixerTitle = ({\n  className,\n  children,\n  ...props\n}: ComponentProps<\"h2\">) => {\n  const { titleId } = useMixerPart(\"MixerTitle\");\n  return (\n    <h2\n      className={cn(\"font-heading mr-auto text-base font-medium\", className)}\n      data-slot=\"mixer-title\"\n      id={titleId}\n      {...props}\n    >\n      {children}\n    </h2>\n  );\n};\n\nexport const MixerActions = ({\n  className,\n  ...props\n}: ComponentProps<\"div\">) => (\n  <div\n    className={cn(\"flex items-center gap-1\", className)}\n    data-slot=\"mixer-actions\"\n    {...props}\n  />\n);\n\nconst focusNeighbour = (\n  event: KeyboardEvent<HTMLDivElement>,\n  direction: number\n) => {\n  const target = event.target as HTMLElement;\n  const strip = target.closest<HTMLElement>(STRIP_SELECTOR);\n  if (!strip) {\n    return false;\n  }\n  const container = event.currentTarget;\n  const strips = [...container.querySelectorAll<HTMLElement>(STRIP_SELECTOR)];\n  const neighbour = strips[strips.indexOf(strip) + direction];\n  if (!neighbour) {\n    return false;\n  }\n  const slot = target.closest<HTMLElement>(\"[data-slot]\")?.dataset.slot;\n  const match = slot\n    ? neighbour.querySelector<HTMLElement>(`[data-slot='${slot}']`)\n    : null;\n  const focusTarget = match?.matches(FOCUSABLE)\n    ? match\n    : (match?.querySelector<HTMLElement>(FOCUSABLE) ??\n      neighbour.querySelector<HTMLElement>(FOCUSABLE));\n  if (!focusTarget) {\n    return false;\n  }\n  focusTarget.focus();\n  return true;\n};\n\nexport interface MixerChannelsProps extends ComponentProps<\"div\"> {\n  /** Scroll along the strip axis when strips overflow. Default true. */\n  scrollable?: boolean;\n}\n\nexport const MixerChannels = ({\n  scrollable = true,\n  className,\n  children,\n  onKeyDownCapture,\n  ...props\n}: MixerChannelsProps) => {\n  const { orientation } = useMixerPart(\"MixerChannels\");\n\n  const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n    onKeyDownCapture?.(event);\n    if (event.defaultPrevented || !(event.ctrlKey || event.metaKey)) {\n      return;\n    }\n    const previous = orientation === \"horizontal\" ? \"ArrowUp\" : \"ArrowLeft\";\n    const next = orientation === \"horizontal\" ? \"ArrowDown\" : \"ArrowRight\";\n    let direction = 0;\n    if (event.key === previous) {\n      direction = -1;\n    } else if (event.key === next) {\n      direction = 1;\n    }\n    if (direction !== 0 && focusNeighbour(event, direction)) {\n      event.preventDefault();\n      event.stopPropagation();\n    }\n  };\n\n  return (\n    <div\n      className={cn(\n        \"flex min-h-0 min-w-0 gap-(--mixer-gap) [grid-area:channels]\",\n        orientation === \"horizontal\" ? \"flex-col\" : \"flex-row\",\n        scrollable &&\n          (orientation === \"horizontal\"\n            ? \"overflow-y-auto\"\n            : \"overflow-x-auto\"),\n        \"empty:hidden\",\n        className\n      )}\n      data-slot=\"mixer-channels\"\n      onKeyDownCapture={handleKeyDown}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n};\n\nexport const MixerSeparator = ({\n  className,\n  ...props\n}: ComponentProps<\"div\">) => {\n  const { orientation } = useMixerPart(\"MixerSeparator\");\n  return (\n    <div\n      aria-hidden\n      className={cn(\n        \"bg-border shrink-0 [grid-area:separator]\",\n        orientation === \"horizontal\" ? \"h-px w-full\" : \"h-full w-px\",\n        className\n      )}\n      data-slot=\"mixer-separator\"\n      {...props}\n    />\n  );\n};\n\nexport const MixerMaster = ({ className, ...props }: ComponentProps<\"div\">) => {\n  const { orientation } = useMixerPart(\"MixerMaster\");\n  return (\n    <div\n      className={cn(\n        \"flex min-h-0 min-w-0 [grid-area:master]\",\n        orientation === \"horizontal\" ? \"flex-col\" : \"flex-row\",\n        className\n      )}\n      data-slot=\"mixer-master\"\n      {...props}\n    />\n  );\n};\n\n/** Shown when `MixerChannels` renders nothing. */\nexport const MixerEmpty = ({ className, ...props }: ComponentProps<\"div\">) => {\n  useMixerPart(\"MixerEmpty\");\n  return (\n    <div\n      className={cn(\n        \"text-muted-foreground hidden min-h-24 items-center justify-center rounded-xl border border-dashed p-6 text-center text-sm [grid-area:channels] group-has-[[data-slot=mixer-channels]:empty]/mixer:flex\",\n        className\n      )}\n      data-slot=\"mixer-empty\"\n      {...props}\n    />\n  );\n};\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}