{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "core-scale-remap-control",
  "type": "registry:ui",
  "title": "Scale Remap Control",
  "description": "A headless controlled Scale/12-TET note-remap switch with compact and circle-of-fifths root-note selectors.",
  "dependencies": [
    "maruaudio@>=0.0.3 <0.1.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/ui/scale-remap-control.tsx",
      "content": "/**\n * Source: https://maru.audio/ui-kit/scale-remap-control\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 type { CSSProperties, ReactNode } from \"react\"\nimport {\n  normalizePitchClass,\n  PITCH_CLASS_NAMES,\n} from \"maruaudio/ui/shared/music-theory-pitch-class\"\nimport { token } from \"maruaudio/ui/shared/theme\"\n\nconst ROOT_BORDER = `var(--scale-remap-border, ${token.border})`\nconst ROOT_SURFACE = `var(--scale-remap-surface, ${token.cap})`\nconst ROOT_INK = \"var(--scale-remap-ink, currentColor)\"\nconst ROOT_ACCENT = `var(--scale-remap-accent, ${token.accent})`\nconst ROOT_ACTIVE_INK =\n  \"var(--scale-remap-active-ink, var(--uikit-accent-contrast, #ffffff))\"\n\n/** Portable fifths-clock states; layout remains inline without a host stylesheet. */\nexport const ROOT_NOTE_SELECTOR_STYLES = String.raw`\n[data-root-note-selector=\"fifths\"] [data-root-note]{background:${ROOT_SURFACE};color:${ROOT_INK};border-color:${ROOT_BORDER};cursor:pointer}\n[data-root-note-selector=\"fifths\"] [data-root-note][aria-pressed=\"true\"]{background:${ROOT_ACCENT};border-color:${ROOT_ACCENT};color:${ROOT_ACTIVE_INK}}\n[data-root-note-selector=\"fifths\"] [data-root-note]:focus-visible{outline:2px solid var(--scale-remap-accent, ${token.focus});outline-offset:2px}\n[data-root-note-selector=\"fifths\"] [data-root-note]:disabled{opacity:.45;cursor:not-allowed}\n@media (hover: hover){\n[data-root-note-selector=\"fifths\"] [data-root-note]:hover:not(:disabled):not([aria-pressed=\"true\"]){background:var(--scale-remap-hover, color-mix(in srgb, ${ROOT_INK} 12%, ${ROOT_SURFACE}))}\n}\n[data-root-note-selector=\"fifths\"] [data-root-note]:active:not(:disabled){filter:brightness(.92)}\n@media (prefers-reduced-motion: no-preference){\n[data-root-note-selector=\"fifths\"] [data-root-note]{transition:background-color 120ms ease-out,border-color 120ms ease-out,color 120ms ease-out}\n}\n`.replace(/\\n/g, \"\")\n\nexport type ScaleRemapMode = \"scale\" | \"chromatic\"\n\n/** Sharp root names, index 0 = C. The canonical music-theory table. */\nexport const ROOT_NOTE_NAMES = PITCH_CLASS_NAMES\n\nexport function normalizeRootNote(root: number): number {\n  return normalizePitchClass(Number.isFinite(root) ? Math.round(root) : 0)\n}\n\n/** C, G, D... in clockwise circle-of-fifths order. */\nexport function rootNotesInFifths(): number[] {\n  return Array.from({ length: 12 }, (_, index) => (index * 7) % 12)\n}\n\nexport interface ScaleRemapControlOptions {\n  mode: ScaleRemapMode\n  root: number\n  onModeChange: (mode: ScaleRemapMode) => void\n  onRootChange: (root: number) => void\n  disabled?: boolean\n}\n\nexport interface ScaleRemapControlSlot {\n  mode: ScaleRemapMode\n  root: number\n  disabled: boolean\n  scaleEnabled: boolean\n  setMode: (mode: ScaleRemapMode) => void\n  setRoot: (root: number) => void\n  toggleMode: () => void\n}\n\nexport function useScaleRemapControl({\n  mode,\n  root,\n  onModeChange,\n  onRootChange,\n  disabled = false,\n}: ScaleRemapControlOptions): ScaleRemapControlSlot {\n  const normalizedRoot = normalizeRootNote(root)\n  const setMode = (next: ScaleRemapMode) => {\n    if (!disabled) onModeChange(next)\n  }\n  const setRoot = (next: number) => {\n    if (!disabled) onRootChange(normalizeRootNote(next))\n  }\n  return {\n    mode,\n    root: normalizedRoot,\n    disabled,\n    scaleEnabled: mode === \"scale\",\n    setMode,\n    setRoot,\n    toggleMode: () => setMode(mode === \"scale\" ? \"chromatic\" : \"scale\"),\n  }\n}\n\nexport function ScaleRemapControlHeadless({\n  children,\n  ...options\n}: ScaleRemapControlOptions & {\n  children: (slot: ScaleRemapControlSlot) => ReactNode\n}) {\n  return children(useScaleRemapControl(options))\n}\n\nexport interface RootNoteSelectorProps {\n  root: number\n  onRootChange: (root: number) => void\n  disabled?: boolean\n  /** A compact native selector or the musician-facing fifths clock. */\n  variant?: \"select\" | \"fifths\"\n  label?: string\n  size?: number\n  className?: string\n  style?: CSSProperties\n}\n\n/** Controlled root-note selector with compact and circle-of-fifths layouts. */\nexport function RootNoteSelector({\n  root,\n  onRootChange,\n  disabled = false,\n  variant = \"select\",\n  label = \"Root\",\n  size = 168,\n  className,\n  style,\n}: RootNoteSelectorProps) {\n  const selected = normalizeRootNote(root)\n  if (variant === \"select\") {\n    return (\n      <label\n        className={`uikit-control inline-flex items-center gap-2${className ? ` ${className}` : \"\"}`}\n        style={style}\n      >\n        <span className=\"text-[10px] font-semibold tracking-[0.14em] uppercase opacity-70\">\n          {label}\n        </span>\n        <select\n          value={selected}\n          disabled={disabled}\n          aria-label={`${label} note`}\n          onChange={(event) => onRootChange(Number(event.target.value))}\n          className=\"h-8 rounded-md border border-[var(--scale-remap-border,rgba(127,127,127,0.4))] bg-[var(--scale-remap-surface,rgba(127,127,127,0.12))] px-2 text-xs font-semibold text-[var(--scale-remap-ink,currentColor)] outline-none focus-visible:ring-2 focus-visible:ring-[var(--scale-remap-accent,currentColor)] disabled:opacity-45\"\n        >\n          {ROOT_NOTE_NAMES.map((name, pitchClass) => (\n            <option key={name} value={pitchClass}>\n              {name}\n            </option>\n          ))}\n        </select>\n      </label>\n    )\n  }\n\n  const nodeSize = Math.max(24, Math.round(size * 0.19))\n  const radius = (size - nodeSize) / 2\n  return (\n    <fieldset\n      className={`uikit-control relative shrink-0 border-0 p-0${className ? ` ${className}` : \"\"}`}\n      style={{\n        position: \"relative\",\n        flexShrink: 0,\n        border: 0,\n        padding: 0,\n        margin: 0,\n        minWidth: 0,\n        color: token.text,\n        width: size,\n        height: size,\n        ...style,\n      }}\n      data-root-note-selector=\"fifths\"\n      aria-label={`${label} note on the circle of fifths`}\n      disabled={disabled}\n    >\n      <style href=\"uikit-root-note-selector\" precedence=\"default\">\n        {ROOT_NOTE_SELECTOR_STYLES}\n      </style>\n      <legend\n        className=\"sr-only\"\n        style={{\n          position: \"absolute\",\n          width: 1,\n          height: 1,\n          padding: 0,\n          margin: -1,\n          overflow: \"hidden\",\n          clipPath: \"inset(50%)\",\n          whiteSpace: \"nowrap\",\n          border: 0,\n        }}\n      >\n        {label} note\n      </legend>\n      <span\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute rounded-full border border-[var(--scale-remap-border,rgba(127,127,127,0.4))]\"\n        style={{\n          position: \"absolute\",\n          pointerEvents: \"none\",\n          borderRadius: \"50%\",\n          border: `1px solid ${ROOT_BORDER}`,\n          inset: nodeSize / 2,\n        }}\n      />\n      <span\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute inset-0 flex items-center justify-center text-center text-[9px] leading-tight font-semibold tracking-[0.12em] uppercase opacity-60\"\n        style={{\n          position: \"absolute\",\n          pointerEvents: \"none\",\n          inset: 0,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          textAlign: \"center\",\n          fontFamily: token.font,\n          fontSize: 9,\n          lineHeight: 1.2,\n          fontWeight: 600,\n          letterSpacing: \".12em\",\n          textTransform: \"uppercase\",\n          color: ROOT_INK,\n          opacity: 0.6,\n        }}\n      >\n        <span>\n          {label}\n          <br />\n          {ROOT_NOTE_NAMES[selected]}\n        </span>\n      </span>\n      {rootNotesInFifths().map((pitchClass, index) => {\n        const angle = (index / 12) * Math.PI * 2 - Math.PI / 2\n        const x = size / 2 + Math.cos(angle) * radius - nodeSize / 2\n        const y = size / 2 + Math.sin(angle) * radius - nodeSize / 2\n        const active = pitchClass === selected\n        return (\n          <button\n            key={pitchClass}\n            type=\"button\"\n            disabled={disabled}\n            aria-label={`${ROOT_NOTE_NAMES[pitchClass]} root`}\n            aria-pressed={active}\n            data-root-note={pitchClass}\n            onClick={() => onRootChange(pitchClass)}\n            className=\"absolute inline-flex items-center justify-center rounded-full border text-[10px] font-semibold transition-[background-color,border-color,color,transform] outline-none focus-visible:ring-2 focus-visible:ring-[var(--scale-remap-accent,currentColor)] active:scale-95 disabled:opacity-45 aria-pressed:border-[var(--scale-remap-accent,currentColor)] aria-pressed:bg-[var(--scale-remap-accent,currentColor)] aria-pressed:text-[var(--scale-remap-active-ink,#111)] data-[root-note]:border-[var(--scale-remap-border,rgba(127,127,127,0.4))]\"\n            style={{\n              position: \"absolute\",\n              display: \"inline-flex\",\n              alignItems: \"center\",\n              justifyContent: \"center\",\n              boxSizing: \"border-box\",\n              borderWidth: 1,\n              borderStyle: \"solid\",\n              borderRadius: \"50%\",\n              padding: 0,\n              fontFamily: token.font,\n              fontSize: 10,\n              lineHeight: 1,\n              fontWeight: 600,\n              left: x,\n              top: y,\n              width: nodeSize,\n              height: nodeSize,\n            }}\n          >\n            {ROOT_NOTE_NAMES[pitchClass]}\n          </button>\n        )\n      })}\n    </fieldset>\n  )\n}\n\nexport interface ScaleRemapControlProps extends ScaleRemapControlOptions {\n  rootVariant?: RootNoteSelectorProps[\"variant\"]\n  rootLabel?: string\n  rootSize?: number\n  className?: string\n  style?: CSSProperties\n}\n\n/** Shared two-state Scale/12-TET control plus a root selector. */\nexport function ScaleRemapControl({\n  rootVariant = \"select\",\n  rootLabel,\n  rootSize,\n  className,\n  style,\n  ...options\n}: ScaleRemapControlProps) {\n  const slot = useScaleRemapControl(options)\n  return (\n    <div\n      className={`uikit-control flex flex-wrap items-center gap-2${className ? ` ${className}` : \"\"}`}\n      style={style}\n      data-note-remap-mode={slot.mode}\n    >\n      <button\n        type=\"button\"\n        role=\"switch\"\n        aria-checked={slot.scaleEnabled}\n        aria-label={\n          slot.scaleEnabled ? \"Use 12-TET notes\" : \"Limit notes to scale\"\n        }\n        disabled={slot.disabled}\n        onClick={slot.toggleMode}\n        className=\"inline-flex h-8 min-w-[5.5rem] items-center justify-center rounded-md border border-[var(--scale-remap-border,rgba(127,127,127,0.4))] bg-[var(--scale-remap-surface,rgba(127,127,127,0.12))] px-2.5 text-[10px] font-semibold tracking-[0.12em] uppercase transition-[background-color,border-color,color,transform] outline-none hover:bg-[var(--scale-remap-hover,rgba(127,127,127,0.2))] focus-visible:ring-2 focus-visible:ring-[var(--scale-remap-accent,currentColor)] active:translate-y-px disabled:opacity-45 aria-checked:border-[var(--scale-remap-accent,currentColor)] aria-checked:text-[var(--scale-remap-accent,currentColor)]\"\n      >\n        {slot.scaleEnabled ? \"In scale\" : \"12-TET\"}\n      </button>\n      <RootNoteSelector\n        root={slot.root}\n        onRootChange={slot.setRoot}\n        disabled={slot.disabled}\n        variant={rootVariant}\n        label={rootLabel}\n        size={rootSize}\n      />\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "@ui/scale-remap-control.tsx"
    }
  ]
}
