{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "core-microphone-pattern-selector",
  "type": "registry:ui",
  "title": "Microphone Pattern Selector",
  "description": "A compact polar-pattern readout opening five visual cards with responsive grid navigation and portable theme inheritance.",
  "dependencies": [
    "@base-ui/react",
    "maruaudio@>=0.0.3 <0.1.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/ui/microphone-pattern-selector.tsx",
      "content": "/**\n * Source: https://maru.audio/ui-kit/microphone-pattern-selector\n * Copyright © [Maru](https://maru.audio)\n *\n * Maru UI Kit — https://maru.audio/ui-kit\n *\n * Installed from the Maru UI registry. This file is yours now: edit it\n * freely, it will not be overwritten unless you re-run the add command.\n *\n * Keep this item’s companion files and CSS imports together. Dependencies\n * declared by the registry are installed with the source.\n */\nimport { selectGridKeyDown } from \"maruaudio/ui/shared/select-grid\"\nimport { useRef, useState, type CSSProperties, type ReactNode } from \"react\"\nimport { Select } from \"@base-ui/react/select\"\nimport {\n  OVERLAY_POSITIONER_DEFAULTS,\n  useOverlayLayer,\n  mergeOverlayPopupStyle,\n} from \"maruaudio/ui/shared/overlay-container\"\nimport {\n  MICROPHONE_PATTERNS,\n  MICROPHONE_PATTERN_LABELS,\n  microphonePatternPath,\n  type MicrophonePattern,\n} from \"maruaudio/ui/shared/microphone-patterns\"\n\nconst MICROPHONE_PORTAL_VARIABLES = [\n  \"accent\",\n  \"accent2\",\n  \"surface\",\n  \"cap\",\n  \"border\",\n  \"text\",\n  \"text-muted\",\n  \"focus\",\n  \"radius\",\n  \"font\",\n].map((key) => `--uikit-${key}`)\n\nexport function MicrophonePatternGlyph({\n  pattern,\n  size = 48,\n}: {\n  pattern: MicrophonePattern\n  size?: number\n}) {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      focusable=\"false\"\n      viewBox=\"0 0 64 64\"\n      width={size}\n      height={size}\n      fill=\"none\"\n    >\n      <circle cx=\"32\" cy=\"32\" r=\"24\" stroke=\"currentColor\" opacity=\"0.15\" />\n      <path d=\"M32 5v54M5 32h54\" stroke=\"currentColor\" opacity=\"0.15\" />\n      <path\n        d={microphonePatternPath(pattern)}\n        fill=\"currentColor\"\n        fillOpacity=\"0.12\"\n        stroke=\"currentColor\"\n        strokeWidth=\"1.6\"\n        vectorEffect=\"non-scaling-stroke\"\n      />\n      <circle cx=\"32\" cy=\"32\" r=\"2\" fill=\"currentColor\" />\n      <path d=\"m29 4 3-3 3 3\" stroke=\"currentColor\" strokeWidth=\"1.2\" />\n    </svg>\n  )\n}\n\nexport interface MicrophoneSelectorOption<T extends string> {\n  readonly value: T\n  readonly label?: string\n  readonly disabled?: boolean\n}\nexport type MicrophoneSelectorOptionInput<T extends string> =\n  T | MicrophoneSelectorOption<T>\n\n/** Base UI owns part rendering, focus and selection; children remain domain-owned. */\nexport interface MicrophoneSelectorParts {\n  triggerProps?: Omit<Select.Trigger.Props, \"children\" | \"disabled\">\n  popupProps?: Omit<Select.Popup.Props, \"children\">\n  positionerProps?: Omit<Select.Positioner.Props, \"children\">\n  portalProps?: Omit<Select.Portal.Props, \"children\">\n}\n\nexport interface MicrophonePatternSelectorProps extends MicrophoneSelectorParts {\n  renderGlyph?: (pattern: MicrophonePattern, size: number) => ReactNode\n  value: MicrophonePattern\n  onValueChange: (pattern: MicrophonePattern) => void\n  options?: readonly MicrophoneSelectorOptionInput<MicrophonePattern>[]\n  label?: string\n  labelHidden?: boolean\n  disabled?: boolean\n  className?: string\n  style?: CSSProperties\n}\nconst styles = `\n[data-mic-pattern-selector]{width:100%;display:flex;flex-direction:column;gap:5px;min-width:0;font-family:var(--uikit-font,system-ui,sans-serif)}\n[data-mic-pattern-trigger]{display:flex;width:100%;align-items:center;justify-content:center;min-height:96px;padding:6px 8px;border:0;border-radius:var(--uikit-radius,0px);background:transparent;color:var(--uikit-text,#edece6);font:inherit;cursor:pointer}\n[data-mic-pattern-trigger]:hover,[data-mic-pattern-trigger][data-popup-open]{background:color-mix(in srgb,var(--uikit-text,#edece6) 5%,transparent)}\n[data-mic-pattern-value]{display:flex;flex-direction:column;align-items:center;gap:4px;font-size:14px;font-weight:500;line-height:1.2}\n[data-mic-pattern-trigger]:disabled{opacity:.45;cursor:not-allowed}\n[data-mic-pattern-menu]{box-sizing:border-box;width:min(340px,calc(100vw - 24px));padding:6px;border:1px solid var(--uikit-border,#535957);border-radius:var(--uikit-radius,0px);background:var(--uikit-cap,#292e30);color:var(--uikit-text,#edece6);font:12px var(--uikit-font,system-ui,sans-serif);box-shadow:0 18px 40px rgb(0 0 0 / .35)}\n[data-mic-pattern-grid]{display:grid;grid-template-columns:repeat(var(--mic-card-columns,3),minmax(0,1fr));gap:5px}\n[data-mic-pattern-option]{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;min-width:0;min-height:91px;padding:8px 3px;border:1px solid var(--uikit-border,#535957);border-radius:var(--uikit-radius,0px);cursor:pointer;font:inherit;font-size:11px}\n[data-mic-pattern-option] svg,[data-mic-pattern-value] svg{color:var(--uikit-accent,#7db7ad);flex-shrink:0}\n[data-mic-pattern-option][data-disabled]{opacity:.4;cursor:not-allowed}\n[data-mic-pattern-option][data-highlighted]{background:color-mix(in srgb,var(--uikit-accent,#7db7ad) 12%,transparent)}\n[data-mic-pattern-option][data-selected]{border-color:var(--uikit-accent,#7db7ad);background:color-mix(in srgb,var(--uikit-accent,#7db7ad) 16%,transparent)}\n[data-mic-pattern-trigger]:focus-visible,[data-mic-pattern-option]:focus-visible{outline:2px solid var(--uikit-focus,var(--uikit-accent,#7db7ad));outline-offset:2px}\n@media(max-width:420px){[data-mic-pattern-grid]{grid-template-columns:repeat(2,minmax(0,1fr))}}\n`\nexport function MicrophoneCardSelector<T extends string>({\n  value,\n  onValueChange,\n  options: optionInputs,\n  labels,\n  renderGlyph,\n  columns = 3,\n  label,\n  labelHidden = false,\n  disabled = false,\n  className,\n  style,\n  triggerProps,\n  popupProps,\n  positionerProps,\n  portalProps,\n}: MicrophoneSelectorParts & {\n  value: T\n  onValueChange: (value: T) => void\n  options: readonly MicrophoneSelectorOptionInput<T>[]\n  labels: Record<T, string>\n  renderGlyph: (value: T, size: number) => ReactNode\n  columns?: 2 | 3\n  label: string\n  labelHidden?: boolean\n  disabled?: boolean\n  className?: string\n  style?: CSSProperties\n}) {\n  const root = useRef<HTMLDivElement>(null)\n  const [open, setOpen] = useState(false)\n  const layer = useOverlayLayer(root, open, MICROPHONE_PORTAL_VARIABLES)\n  const options = optionInputs.map((option) =>\n    typeof option === \"string\" ? { value: option } : option\n  )\n  const selectedLabel =\n    options.find((option) => option.value === value)?.label ?? labels[value]\n  const unavailable = disabled || options.every((option) => option.disabled)\n  return (\n    <Select.Root\n      value={value}\n      disabled={unavailable}\n      modal={false}\n      items={options.map((option) => ({\n        value: option.value,\n        label: option.label ?? labels[option.value],\n      }))}\n      onValueChange={(next) => {\n        if (next !== null) onValueChange(next as T)\n      }}\n      open={open}\n      onOpenChange={setOpen}\n    >\n      <div\n        ref={root}\n        data-mic-pattern-selector\n        className={className}\n        style={style}\n      >\n        <style>{styles}</style>\n        {!labelHidden && <span>{label}</span>}\n        <Select.Trigger\n          data-mic-pattern-trigger\n          aria-label={`${label}: ${selectedLabel}`}\n          {...triggerProps}\n        >\n          <Select.Value>\n            {() => (\n              <span data-mic-pattern-value>\n                {renderGlyph(value, 64)}\n                <span>{selectedLabel}</span>\n              </span>\n            )}\n          </Select.Value>\n        </Select.Trigger>\n        <Select.Portal\n          {...portalProps}\n          container={portalProps?.container ?? layer.container ?? undefined}\n        >\n          <Select.Positioner\n            {...OVERLAY_POSITIONER_DEFAULTS}\n            alignItemWithTrigger={false}\n            side=\"bottom\"\n            align=\"start\"\n            sideOffset={6}\n            collisionPadding={12}\n            {...positionerProps}\n            style={(state) =>\n              ({\n                ...layer.theme,\n                \"--mic-card-columns\": columns,\n                zIndex: \"var(--uikit-z-overlay,2147483647)\",\n                ...(typeof positionerProps?.style === \"function\"\n                  ? positionerProps.style(state)\n                  : positionerProps?.style),\n              }) as CSSProperties\n            }\n          >\n            <style>{styles}</style>\n            <Select.Popup\n              data-mic-pattern-menu\n              {...popupProps}\n              style={(state) => ({\n                ...mergeOverlayPopupStyle(\n                  {\n                    overflow: \"visible\",\n                    ...(typeof popupProps?.style === \"function\"\n                      ? popupProps.style(state)\n                      : popupProps?.style),\n                  },\n                  { maxHeight: \"none\" }\n                ),\n              })}\n            >\n              <Select.List\n                data-mic-pattern-grid\n                aria-label={`${label} choices`}\n                onKeyDown={(event) =>\n                  selectGridKeyDown(event, \"[data-mic-pattern-option]\", columns)\n                }\n              >\n                {options.map((option) => (\n                  <Select.Item\n                    key={option.value}\n                    value={option.value}\n                    label={option.label ?? labels[option.value]}\n                    disabled={option.disabled}\n                    data-mic-pattern-option\n                    data-selected={option.value === value ? \"\" : undefined}\n                  >\n                    {renderGlyph(option.value, 54)}\n                    <Select.ItemText>\n                      {option.label ?? labels[option.value]}\n                    </Select.ItemText>\n                  </Select.Item>\n                ))}\n              </Select.List>\n            </Select.Popup>\n          </Select.Positioner>\n        </Select.Portal>\n      </div>\n    </Select.Root>\n  )\n}\n\nexport function MicrophonePatternSelector({\n  options = MICROPHONE_PATTERNS,\n  label = \"Pattern\",\n  renderGlyph = (pattern, size) => (\n    <MicrophonePatternGlyph pattern={pattern} size={size} />\n  ),\n  ...props\n}: MicrophonePatternSelectorProps) {\n  return (\n    <MicrophoneCardSelector\n      {...props}\n      options={options}\n      labels={MICROPHONE_PATTERN_LABELS}\n      label={label}\n      renderGlyph={renderGlyph}\n    />\n  )\n}\n",
      "type": "registry:component",
      "target": "@ui/microphone-pattern-selector.tsx"
    }
  ]
}
