{
  "name": "audiogram-bars",
  "type": "registry:ui",
  "description": "Audio-reactive spectrum bar visualization",
  "dependencies": [
    "remotion",
    "@remotion/media-utils"
  ],
  "registryDependencies": [
    "audio-viz-utils"
  ],
  "files": [
    {
      "path": "registry/bases/default/primitives/audiogram-bars.tsx",
      "type": "registry:ui",
      "content": "import { useAudioBands } from \"@/remotion/lib/audio-viz-utils\";\n\nexport type AudiogramBarsProps = {\n  src: string;\n  height?: number;\n  barColor?: string;\n  /** Second colour for the gradient across the spectrum. Defaults to `barColor`. */\n  barColorEnd?: string;\n  barGap?: number;\n  numberOfSamples?: number;\n  maxBarCount?: number;\n  /** `bottom` grows bars from the baseline, `center` mirrors them around it. */\n  align?: \"bottom\" | \"center\";\n  /** Draws the decaying peak cap above each bar. */\n  showPeaks?: boolean;\n  /** Mirrored, faded copy below the baseline. Ignored when `align` is `center`. */\n  showReflection?: boolean;\n  /**\n   * Optional frame override.\n   * Pass a parent `frame` when using inside `<Sequence from={...}>` to avoid discontinuities.\n   */\n  frame?: number;\n};\n\nconst clamp01 = (value: number) => Math.min(1, Math.max(0, value));\nconst lerp = (a: number, b: number, t: number) => a + (b - a) * t;\n\n/** Blends two hex colours so a gradient can be sampled per bar. */\nfunction mixHex(from: string, to: string, t: number) {\n  const parse = (hex: string) => {\n    const value = hex.replace(\"#\", \"\");\n    const full =\n      value.length === 3\n        ? value\n            .split(\"\")\n            .map((char) => char + char)\n            .join(\"\")\n        : value.slice(0, 6);\n    return [\n      Number.parseInt(full.slice(0, 2), 16),\n      Number.parseInt(full.slice(2, 4), 16),\n      Number.parseInt(full.slice(4, 6), 16),\n    ];\n  };\n\n  const [r1, g1, b1] = parse(from);\n  const [r2, g2, b2] = parse(to);\n\n  if ([r1, g1, b1, r2, g2, b2].some((channel) => Number.isNaN(channel))) {\n    return from;\n  }\n\n  const channel = (a: number, b: number) =>\n    Math.round(lerp(a, b, clamp01(t)))\n      .toString(16)\n      .padStart(2, \"0\");\n\n  return `#${channel(r1, r2)}${channel(g1, g2)}${channel(b1, b2)}`;\n}\n\ntype BarProps = {\n  value: number;\n  peak: number;\n  color: string;\n  align: \"bottom\" | \"center\";\n  showPeaks: boolean;\n  showReflection: boolean;\n};\n\nfunction Bar({\n  value,\n  peak,\n  color,\n  align,\n  showPeaks,\n  showReflection,\n}: BarProps) {\n  // A visible floor keeps the baseline legible during silence instead of\n  // leaving a gap where the spectrum used to be.\n  const level = Math.max(0.035, clamp01(value));\n  const cap = Math.max(level, clamp01(peak));\n  const glow = lerp(0, 18, level);\n  const isCentered = align === \"center\";\n\n  return (\n    <div\n      style={{\n        flex: 1,\n        position: \"relative\",\n        height: \"100%\",\n        display: \"flex\",\n        flexDirection: \"column\",\n        justifyContent: isCentered ? \"center\" : \"flex-end\",\n      }}\n    >\n      <div\n        style={{\n          height: `${level * (isCentered ? 100 : 100)}%`,\n          borderRadius: 999,\n          background: isCentered\n            ? `linear-gradient(to bottom, ${color}33 0%, ${color} 50%, ${color}33 100%)`\n            : `linear-gradient(to top, ${color}b3 0%, ${color} 62%, ${color}f2 100%)`,\n          boxShadow: level > 0.12 ? `0 0 ${Math.round(glow)}px ${color}4d` : undefined,\n        }}\n      />\n      {showReflection && !isCentered ? (\n        <div\n          style={{\n            position: \"absolute\",\n            top: \"100%\",\n            left: 0,\n            right: 0,\n            height: `${level * 34}%`,\n            borderRadius: 999,\n            background: `linear-gradient(to bottom, ${color}3d 0%, transparent 100%)`,\n          }}\n        />\n      ) : null}\n      {showPeaks ? (\n        <div\n          style={{\n            position: \"absolute\",\n            left: 0,\n            right: 0,\n            height: 2,\n            borderRadius: 999,\n            background: color,\n            opacity: 0.55,\n            ...(isCentered\n              ? { top: `${50 - cap * 50}%` }\n              : { bottom: `${cap * 100}%` }),\n          }}\n        />\n      ) : null}\n    </div>\n  );\n}\n\n/**\n * Spectrum bars driven by the audio at the current frame.\n *\n * Bands are grouped logarithmically and tilted (see `toSpectrumBands`) so the\n * whole width reacts rather than only the bass end, and levels run through an\n * attack/release follower so transients punch without the display flickering.\n */\nexport const AudiogramBars: React.FC<AudiogramBarsProps> = ({\n  src,\n  height = 120,\n  barColor = \"#e8b86d\",\n  barColorEnd,\n  barGap = 3,\n  numberOfSamples = 128,\n  maxBarCount = 48,\n  align = \"bottom\",\n  showPeaks = true,\n  showReflection = false,\n  frame: frameOverride,\n}) => {\n  const { bands, peaks } = useAudioBands({\n    src,\n    numberOfSamples,\n    bandCount: maxBarCount,\n    frame: frameOverride,\n  });\n\n  const endColor = barColorEnd ?? barColor;\n\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        alignItems: \"stretch\",\n        height,\n        gap: barGap,\n        width: \"100%\",\n      }}\n    >\n      {bands.map((value, index) => (\n        <Bar\n          // Bands are a fixed-length spectrum, so the index is the identity.\n          key={index}\n          value={value}\n          peak={peaks[index] ?? value}\n          color={mixHex(barColor, endColor, index / Math.max(1, bands.length - 1))}\n          align={align}\n          showPeaks={showPeaks}\n          showReflection={showReflection}\n        />\n      ))}\n    </div>\n  );\n};\n"
    }
  ],
  "atlas": {
    "lane": "signals",
    "drive": "media",
    "tier": "advanced",
    "tags": [
      "audio"
    ]
  }
}