{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "core-rate-level-envelope-display",
  "type": "registry:ui",
  "title": "Rate Level Envelope Display",
  "description": "A controlled plot for classic FM rate/level envelopes, where every stage has both a speed and a destination level. Supports any stage count, configurable ranges, a sustain-stage marker, stage labels and the shared UI-kit theme tokens.",
  "dependencies": [
    "maruaudio@>=0.0.3 <0.1.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/ui/rate-level-envelope-display.tsx",
      "content": "/**\n * Source: https://maru.audio/ui-kit/rate-level-envelope-display\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 */\n/**\n * A display for rate/level envelopes used by classic FM instruments.\n *\n * Unlike ADSR, every stage has both a rate and a destination level. The final\n * level is also the starting level for the next strike. The host owns all\n * parameter editing; this component turns the controlled arrays into one\n * consistent, themed plot.\n */\n\nimport { useId } from \"react\"\nimport type { CSSProperties } from \"react\"\nimport { token } from \"maruaudio/ui/shared/theme\"\n\nexport interface RateLevelEnvelopeDisplayProps {\n  /** Stage rates, normally four values on a 0..99 scale. */\n  rates: readonly number[]\n  /** Destination level for each stage. Must align with `rates`. */\n  levels: readonly number[]\n  rateMin?: number\n  rateMax?: number\n  levelMin?: number\n  levelMax?: number\n  /** Stage held before the final release stage. One-based. Defaults to N-1. */\n  sustainStage?: number\n  color?: string\n  gridColor?: string\n  nodeColor?: string\n  labelColor?: string\n  height?: number\n  showStageLabels?: boolean\n  className?: string\n  style?: CSSProperties\n  \"aria-label\"?: string\n}\n\nconst VIEWBOX_WIDTH = 396\nconst VIEWBOX_HEIGHT = 148\nconst PLOT_LEFT = 18\nconst PLOT_RIGHT = 378\nconst PLOT_TOP = 24\nconst PLOT_BOTTOM = 132\n\nfunction finiteOr(value: number | undefined, fallback: number): number {\n  return typeof value === \"number\" && Number.isFinite(value) ? value : fallback\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n  return Math.min(max, Math.max(min, value))\n}\n\nexport function RateLevelEnvelopeDisplay({\n  rates,\n  levels,\n  rateMin = 0,\n  rateMax = 99,\n  levelMin = 0,\n  levelMax = 99,\n  sustainStage,\n  color = token.accent,\n  gridColor = token.border,\n  nodeColor = token.surface,\n  labelColor = token.textMuted,\n  height = VIEWBOX_HEIGHT,\n  showStageLabels = true,\n  className,\n  style,\n  \"aria-label\": ariaLabel,\n}: RateLevelEnvelopeDisplayProps) {\n  const gradientId = `rate-level-envelope-${useId().replace(/:/g, \"\")}`\n  const stageCount = Math.max(1, Math.min(rates.length, levels.length))\n  const safeRateMin = Math.min(rateMin, rateMax)\n  const safeRateMax = Math.max(rateMin, rateMax)\n  const safeLevelMin = Math.min(levelMin, levelMax)\n  const safeLevelMax = Math.max(levelMin, levelMax)\n  const levelSpan = safeLevelMax - safeLevelMin || 1\n\n  const stageRates = Array.from({ length: stageCount }, (_, index) =>\n    clamp(finiteOr(rates[index], safeRateMin), safeRateMin, safeRateMax)\n  )\n  const stageLevels = Array.from({ length: stageCount }, (_, index) =>\n    clamp(finiteOr(levels[index], safeLevelMin), safeLevelMin, safeLevelMax)\n  )\n  const weights = stageRates.map((rate) => Math.sqrt(safeRateMax - rate + 3))\n  const weightSum = weights.reduce((sum, value) => sum + value, 0) || 1\n  const plotWidth = PLOT_RIGHT - PLOT_LEFT\n  const xs = [PLOT_LEFT]\n  let elapsed = PLOT_LEFT\n  for (const weight of weights) {\n    elapsed += (weight / weightSum) * plotWidth\n    xs.push(elapsed)\n  }\n\n  const levelY = (level: number) =>\n    PLOT_BOTTOM -\n    ((level - safeLevelMin) / levelSpan) * (PLOT_BOTTOM - PLOT_TOP)\n  const startingLevel = stageLevels[stageCount - 1] ?? safeLevelMin\n  const ys = [levelY(startingLevel), ...stageLevels.map(levelY)]\n  const points = xs.map((x, index) => `${x},${ys[index]}`).join(\" \")\n  const resolvedSustainStage = clamp(\n    Math.round(sustainStage ?? Math.max(1, stageCount - 1)),\n    1,\n    stageCount\n  )\n\n  const label =\n    ariaLabel ??\n    `${stageCount}-stage rate and level envelope, rates ${stageRates.join(\", \")}, levels ${stageLevels.join(\", \")}`\n\n  return (\n    <figure\n      className={className}\n      style={{\n        margin: 0,\n        border: `1px solid ${token.border}`,\n        borderRadius: token.radius,\n        background: token.track,\n        padding: 8,\n        boxSizing: \"border-box\",\n        fontFamily: token.font,\n        ...style,\n      }}\n    >\n      <svg\n        viewBox={`0 0 ${VIEWBOX_WIDTH} ${VIEWBOX_HEIGHT}`}\n        width=\"100%\"\n        height={height}\n        style={{ display: \"block\" }}\n        role=\"img\"\n        aria-label={label}\n      >\n        <defs>\n          <linearGradient id={gradientId} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n            <stop offset=\"0\" stopColor={color} stopOpacity=\"0.26\" />\n            <stop offset=\"1\" stopColor={color} stopOpacity=\"0.02\" />\n          </linearGradient>\n        </defs>\n        {[24, 51, 78, 105, 132].map((y) => (\n          <line\n            key={y}\n            x1=\"12\"\n            x2=\"384\"\n            y1={y}\n            y2={y}\n            stroke={gridColor}\n            strokeWidth=\"1\"\n            opacity=\"0.55\"\n          />\n        ))}\n        <polygon\n          points={`${PLOT_LEFT},${PLOT_BOTTOM} ${points} ${PLOT_RIGHT},${PLOT_BOTTOM}`}\n          fill={`url(#${gradientId})`}\n        />\n        <polyline\n          points={points}\n          fill=\"none\"\n          stroke={color}\n          strokeWidth=\"2.1\"\n        />\n        {xs.map((x, index) => (\n          <g key={`${index}-${x}`}>\n            <circle\n              cx={x}\n              cy={ys[index]}\n              r=\"4.5\"\n              fill={nodeColor}\n              stroke={color}\n              strokeWidth=\"2\"\n            />\n            {showStageLabels ? (\n              <text\n                x={x}\n                y=\"145\"\n                textAnchor=\"middle\"\n                fill={labelColor}\n                fontSize=\"8\"\n                fontFamily={token.fontMono}\n              >\n                {index === 0\n                  ? `L${stageCount}`\n                  : index === stageCount\n                    ? `R${index}/L${index}`\n                    : `R${index}/L${index}`}\n              </text>\n            ) : null}\n          </g>\n        ))}\n        {resolvedSustainStage < stageCount ? (\n          <line\n            x1={xs[resolvedSustainStage]}\n            x2={xs[resolvedSustainStage + 1]}\n            y1={ys[resolvedSustainStage]}\n            y2={ys[resolvedSustainStage]}\n            stroke={color}\n            strokeDasharray=\"3 3\"\n            opacity=\"0.55\"\n          />\n        ) : null}\n      </svg>\n    </figure>\n  )\n}\n",
      "type": "registry:component",
      "target": "@ui/rate-level-envelope-display.tsx"
    }
  ]
}
