{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "core-note-range-selector",
  "type": "registry:ui",
  "title": "Note Range Selector",
  "description": "A bounded piano range with named notes, gesture commits and an optional instrument-default reset.",
  "dependencies": [
    "@base-ui/react",
    "maruaudio@>=0.0.3 <0.1.0"
  ],
  "registryDependencies": [
    "https://pluginui.com/r/core-console-button.json",
    "https://pluginui.com/r/core-range-slider.json"
  ],
  "files": [
    {
      "path": "components/ui/note-range-selector.tsx",
      "content": "/**\n * Source: https://maru.audio/ui-kit/note-range-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 * as React from \"react\"\nimport { RangeSlider, type RangeSliderProps } from \"@/components/ui/range-slider\"\nimport { normalizeRangeValue, type RangeSliderValue } from \"maruaudio/ui/shared/range-slider-math\"\nimport { ConsoleButton } from \"@/components/ui/console-button\"\nimport { keyNoteName } from \"maruaudio/ui/shared/keyboard-geometry\"\nimport { token } from \"maruaudio/ui/shared/theme\"\n\nexport interface NoteRangeSelectorProps extends Omit<RangeSliderProps,\n  \"keyboard\" | \"step\" | \"largeStep\" | \"minStepsBetweenValues\" | \"showLabels\" | \"onValueChange\"> {\n  /** Lowest keyboard note; defaults to MIDI 21 (A0). */\n  readonly min?: number\n  /** Highest keyboard note; defaults to MIDI 108 (C8). */\n  readonly max?: number\n  /** Optional reset target, in MIDI notes; clamped to the keyboard bounds. */\n  readonly defaultValue?: RangeSliderValue\n  /** Every edit, including Reset. Commit-only consumers need not manage a drag draft. */\n  readonly onValueChange?: (value: [number, number]) => void\n  readonly resetLabel?: string\n}\n\n/** A note span on a bounded keyboard. Defaults to the 88-key piano, A0–C8.\n * `value` is controlled; a local gesture draft lets hosts persist only on commit.\n * Bounds must be whole MIDI notes, 0–127. The host owns instrument semantics.\n */\nexport function NoteRangeSelector({\n  value, min = 21, max = 108, defaultValue, onValueChange, onValueCommitted,\n  resetLabel = \"Reset\", formatValue = keyNoteName, disabled,\n  thumbLabels = [\"Lowest note\", \"Highest note\"],\n  className, style, width = \"100%\", \"aria-label\": label = \"Note range\", ...props\n}: NoteRangeSelectorProps) {\n  if (!Number.isInteger(min) || !Number.isInteger(max) || min < 0 || max > 127 || min >= max)\n    throw new RangeError(\"NoteRangeSelector bounds must be ascending MIDI notes between 0 and 127.\")\n  const [draft, setDraft] = React.useState<[number, number] | null>(null)\n  React.useEffect(() => { setDraft(null) }, [value[0], value[1], min, max, disabled])\n  const selection = draft ?? normalizeRangeValue(value.map(Math.round), min, max)\n  const reset = defaultValue ? normalizeRangeValue(defaultValue.map(Math.round), min, max) : null\n  const atDefault = reset !== null && selection[0] === reset[0] && selection[1] === reset[1]\n  return <div data-slot=\"note-range-selector\" className={className} style={{ width, minWidth: 0, ...style }}>\n    <RangeSlider {...props} value={selection} min={min} max={max} keyboard step={1} largeStep={12}\n      disabled={disabled} formatValue={formatValue} thumbLabels={thumbLabels} aria-label={label}\n      onValueChange={next => { setDraft(next); onValueChange?.(next) }}\n      onValueCommitted={next => { setDraft(null); onValueCommitted?.(next) }} />\n    <div style={{ display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", gap: 8, marginTop: 6,\n      fontSize: 11, color: token.textMuted }}>\n      <span aria-hidden=\"true\">{formatValue(min)} – {formatValue(max)}</span>\n      {reset ? <ConsoleButton size=\"sm\" variant=\"ghost\" disabled={disabled || atDefault}\n        aria-label={`Reset ${label.toLowerCase()}`} onClick={() => {\n          setDraft(null)\n          onValueChange?.(reset)\n          onValueCommitted?.(reset)\n        }}>{resetLabel}</ConsoleButton> : null}\n    </div>\n  </div>\n}\n",
      "type": "registry:component",
      "target": "@ui/note-range-selector.tsx"
    }
  ]
}
