{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "core-waveform-selector",
  "type": "registry:ui",
  "title": "Waveform Selector",
  "description": "A compact waveform readout that opens a visual, keyboard-navigable popover instead of spending panel width on segmented facets. Includes a hook for integer DSP enum params.",
  "dependencies": [
    "@base-ui/react",
    "maruaudio@>=0.0.3 <0.1.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/ui/waveform-selector.tsx",
      "content": "/**\n * Source: https://maru.audio/ui-kit/waveform-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 {\n  useCallback,\n  useRef,\n  useState,\n  type CSSProperties,\n  type ReactNode,\n} from \"react\"\nimport {\n  OVERLAY_POSITIONER_DEFAULTS,\n  mergeOverlayPopupStyle,\n  useOverlayLayer,\n} from \"maruaudio/ui/shared/overlay-container\"\nimport { Select } from \"@base-ui/react/select\"\nimport {\n  DSP_WAVEFORMS,\n  PERIODIC_WAVEFORMS,\n  WAVEFORM_LABELS,\n  waveformAtIndex,\n  waveformIndex,\n  type Waveform,\n} from \"maruaudio/ui/shared/waveforms\"\nimport { UI_KIT_PORTABLE_STYLES } from \"maruaudio/ui/shared/portable-styles\"\n\nconst WAVEFORM_PORTAL_VARIABLES = [\n  \"--uikit-font\",\n  \"--waveform-accent\",\n  \"--waveform-border\",\n  \"--waveform-cap\",\n  \"--waveform-muted\",\n  \"--waveform-radius\",\n  \"--waveform-surface\",\n  \"--waveform-text\",\n] as const\n\nexport interface WaveformOption {\n  value: Waveform\n  label?: string\n  disabled?: boolean\n}\n\nexport type WaveformOptionInput = Waveform | WaveformOption\n\nexport interface UseWaveformParamOptions {\n  value: number\n  onValueChange: (value: number) => void\n  waveforms?: readonly Waveform[]\n}\n\n/** Adapter for the integer enums most DSP schemas expose. */\nexport function useWaveformParam({\n  value,\n  onValueChange,\n  waveforms = DSP_WAVEFORMS,\n}: UseWaveformParamOptions) {\n  const waveform = waveformAtIndex(value, waveforms)\n  const setWaveform = useCallback(\n    (next: Waveform) => onValueChange(waveformIndex(next, waveforms)),\n    [onValueChange, waveforms]\n  )\n  return { value: waveform, onValueChange: setWaveform }\n}\n\nexport interface WaveformSelectorProps {\n  readonly triggerProps?: Omit<Select.Trigger.Props, \"children\" | \"disabled\">\n  readonly popupProps?: Omit<Select.Popup.Props, \"children\">\n  readonly portalProps?: Omit<Select.Portal.Props, \"children\">\n  readonly renderGlyph?: (waveform: Waveform, size: number) => ReactNode\n  readonly positionerProps?: Omit<Select.Positioner.Props, \"children\">\n  value: Waveform\n  onValueChange: (value: Waveform) => void\n  options?: readonly WaveformOptionInput[]\n  label?: string\n  labelHidden?: boolean\n  disabled?: boolean\n  className?: string\n  style?: CSSProperties\n}\n\nfunction normalizedOptions(\n  options: readonly WaveformOptionInput[]\n): WaveformOption[] {\n  return options.map((option) =>\n    typeof option === \"string\" ? { value: option } : option\n  )\n}\n\n/** A small domain glyph. It is a data plot, not a general-purpose icon. */\nexport function WaveformGlyph({\n  waveform,\n  size = 40,\n  strokeWidth = 1.8,\n}: {\n  waveform: Waveform\n  size?: number\n  strokeWidth?: number\n}) {\n  const path =\n    waveform === \"sine\"\n      ? \"M2 12 C6 3 10 3 14 12 S22 21 26 12 S34 3 38 12\"\n      : waveform === \"triangle\"\n        ? \"M2 18 L12 6 L22 18 L32 6 L38 13\"\n        : waveform === \"sawtooth\"\n          ? \"M2 18 L17 6 L17 18 L32 6 L32 18 L38 13\"\n          : waveform === \"square\"\n            ? \"M2 18 L2 6 L18 6 L18 18 L34 18 L34 6 L38 6\"\n            : \"M2 15 L8 15 L8 7 L15 7 L15 18 L23 18 L23 10 L31 10 L31 5 L38 5\"\n  return (\n    <svg\n      aria-hidden=\"true\"\n      viewBox=\"0 0 40 24\"\n      width={size}\n      height={Math.round(size * 0.6)}\n      fill=\"none\"\n      focusable=\"false\"\n    >\n      <path\n        d={path}\n        stroke=\"currentColor\"\n        strokeWidth={strokeWidth}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        vectorEffect=\"non-scaling-stroke\"\n      />\n    </svg>\n  )\n}\n\nexport function WaveformSelector({\n  value,\n  onValueChange,\n  options: optionInputs = PERIODIC_WAVEFORMS,\n  label = \"Waveform\",\n  labelHidden = false,\n  disabled = false,\n  className,\n  style,\n  positionerProps,\n  triggerProps,\n  popupProps,\n  portalProps,\n  renderGlyph = (waveform, size) => (\n    <WaveformGlyph waveform={waveform} size={size} />\n  ),\n}: WaveformSelectorProps) {\n  const options = normalizedOptions(optionInputs)\n  const [open, setOpen] = useState(false)\n  const rootRef = useRef<HTMLDivElement>(null)\n  const selectedLabel =\n    options.find((option) => option.value === value)?.label ??\n    WAVEFORM_LABELS[value]\n  const unavailable = disabled || options.every((option) => option.disabled)\n\n  const layer = useOverlayLayer(rootRef, open, WAVEFORM_PORTAL_VARIABLES)\n\n  return (\n    <Select.Root\n      value={value}\n      open={open}\n      onOpenChange={setOpen}\n      onValueChange={(next) => {\n        if (next !== null) onValueChange(next as Waveform)\n      }}\n      disabled={unavailable}\n      modal={false}\n      items={options.map((option) => ({\n        value: option.value,\n        label: option.label ?? WAVEFORM_LABELS[option.value],\n      }))}\n    >\n      <div\n        ref={rootRef}\n        data-waveform-selector\n        className={`relative inline-flex flex-col gap-1.5 [font-family:var(--uikit-font,ui-sans-serif,system-ui,sans-serif)] ${className ?? \"\"}`}\n        style={style}\n      >\n        {/* Local injection keeps the registry control usable without a host stylesheet. */}\n        <style data-maru-ui-kit-portable>{UI_KIT_PORTABLE_STYLES}</style>\n        <span\n          data-waveform-label\n          className={\n            labelHidden\n              ? \"sr-only\"\n              : \"text-[10px] leading-none font-semibold tracking-[0.08em] text-[var(--waveform-muted)] uppercase\"\n          }\n        >\n          {label}\n        </span>\n        <Select.Trigger\n          aria-label={`${label}: ${selectedLabel}`}\n          disabled={unavailable}\n          data-waveform-trigger\n          className={`uikit-control grid min-h-[54px] min-w-[92px] cursor-pointer grid-cols-[1fr_14px] items-center gap-1 rounded-[var(--waveform-radius)] border border-[var(--waveform-border)] bg-[var(--waveform-surface)] py-1.5 pr-2 pl-2.5 text-[11px] leading-none font-semibold text-[var(--waveform-text)] transition-[background-color,box-shadow,transform] hover:bg-[color-mix(in_srgb,var(--waveform-accent)_8%,var(--waveform-surface))] active:translate-y-px disabled:cursor-not-allowed disabled:opacity-45 data-[popup-open]:[box-shadow:0_0_0_2px_color-mix(in_srgb,var(--waveform-accent)_28%,transparent)]`}\n          {...triggerProps}\n        >\n          <Select.Value>\n            {(selected) => (\n              <span\n                data-waveform-value\n                className=\"grid justify-items-center gap-1\"\n              >\n                <span\n                  data-waveform-glyph\n                  className=\"text-[var(--waveform-accent)]\"\n                >\n                  {renderGlyph((selected ?? value) as Waveform, 42)}\n                </span>\n                <span>{selectedLabel}</span>\n              </span>\n            )}\n          </Select.Value>\n          <span\n            aria-hidden=\"true\"\n            data-waveform-caret\n            className=\"grid gap-px text-[8px] text-[var(--waveform-muted)]\"\n          >\n            <span>▲</span>\n            <span>▼</span>\n          </span>\n        </Select.Trigger>\n        <Select.Portal\n          {...portalProps}\n          container={portalProps?.container ?? layer.container ?? undefined}\n        >\n          <Select.Positioner\n            {...OVERLAY_POSITIONER_DEFAULTS}\n            // A grid has no vertical item/trigger alignment; use collision positioning.\n            alignItemWithTrigger={false}\n            side=\"bottom\"\n            align=\"start\"\n            sideOffset={6}\n            collisionPadding={8}\n            className=\"z-[100] outline-none\"\n            {...positionerProps}\n          >\n            <Select.Popup\n              data-waveform-menu\n              className=\"m-0 grid w-[252px] max-w-[calc(100dvw-16px)] grid-cols-2 gap-1 rounded-[var(--waveform-radius)] border border-[var(--waveform-border)] bg-[var(--waveform-cap)] p-1.5 text-[11px] leading-none font-semibold text-[var(--waveform-text)] [box-shadow:0_14px_34px_rgba(0,0,0,0.35)] backdrop:bg-transparent\"\n              {...popupProps}\n              style={(state) => ({\n                ...layer.theme,\n                width: 252,\n                inset: \"auto\",\n                display: \"grid\",\n                ...mergeOverlayPopupStyle(\n                  typeof popupProps?.style === \"function\"\n                    ? popupProps.style(state)\n                    : popupProps?.style\n                ),\n              })}\n            >\n              <Select.List\n                aria-label={`${label} choices`}\n                className=\"contents\"\n                style={{ display: \"contents\" }}\n                onKeyDown={(event) =>\n                  selectGridKeyDown(event, \"[data-waveform-option]\", 2)\n                }\n              >\n                {options.map((option) => {\n                  const selected = option.value === value\n                  return (\n                    <Select.Item\n                      key={option.value}\n                      value={option.value}\n                      label={option.label ?? WAVEFORM_LABELS[option.value]}\n                      data-waveform-option\n                      disabled={option.disabled}\n                      className={`grid min-h-16 cursor-pointer place-items-center gap-1.5 rounded-[calc(var(--waveform-radius)-2px)] border p-2 font-[inherit] transition-[background-color,border-color,transform] hover:bg-[color-mix(in_srgb,var(--waveform-accent)_10%,transparent)] active:translate-y-px data-[disabled]:cursor-not-allowed data-[disabled]:opacity-40 data-[highlighted]:bg-[color-mix(in_srgb,var(--waveform-accent)_10%,transparent)] ${\n                        selected\n                          ? \"border-[var(--waveform-accent)] bg-[color-mix(in_srgb,var(--waveform-accent)_16%,transparent)] text-[var(--waveform-accent)]\"\n                          : \"border-transparent text-[var(--waveform-text)]\"\n                      }`}\n                    >\n                      {renderGlyph(option.value, 48)}\n                      <span>\n                        {option.label ?? WAVEFORM_LABELS[option.value]}\n                      </span>\n                    </Select.Item>\n                  )\n                })}\n              </Select.List>\n            </Select.Popup>\n          </Select.Positioner>\n        </Select.Portal>\n      </div>\n    </Select.Root>\n  )\n}\n",
      "type": "registry:component",
      "target": "@ui/waveform-selector.tsx"
    }
  ]
}
