{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "indicator-chart",
  "title": "Indicator Chart",
  "description": "Price, RSI and MACD stacked on one time axis with a single crosshair that reports all three panes at once.",
  "files": [
    {
      "path": "app/registry/charts/chart-engine.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\nimport { cn } from '@/lib/utils';\n\nexport const marketVarsClassName =\n  '[--spectrum-chart-up:#059669] [--spectrum-chart-down:#e11d48] [--spectrum-chart-surface:#fff] dark:[--spectrum-chart-up:#34d399] dark:[--spectrum-chart-down:#fb7185] dark:[--spectrum-chart-surface:#0a0a0a]';\n\nexport const UP = 'var(--spectrum-chart-up)';\nexport const DOWN = 'var(--spectrum-chart-down)';\nexport const SURFACE = 'var(--spectrum-chart-surface)';\nexport const EASE = 'cubic-bezier(0.22, 1, 0.36, 1)';\n\nexport type Candle = {\n  t: number;\n  open: number;\n  high: number;\n  low: number;\n  close: number;\n  volume: number;\n};\n\nexport type MarketRange = {\n  label: string;\n  bars: number | null;\n};\n\nexport const MARKET_RANGES: MarketRange[] = [\n  { label: '1W', bars: 7 },\n  { label: '1M', bars: 30 },\n  { label: '3M', bars: 90 },\n  { label: '6M', bars: 180 },\n  { label: '1Y', bars: 365 },\n  { label: 'ALL', bars: null },\n];\n\nexport function mulberry32(seed: number) {\n  let a = seed;\n  return () => {\n    a |= 0;\n    a = (a + 0x6d2b79f5) | 0;\n    let t = Math.imul(a ^ (a >>> 15), 1 | a);\n    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\nexport const DAY_MS = 86_400_000;\n\nexport function generateCandles({\n  seed,\n  count,\n  start,\n  drift,\n  vol,\n  startedAt,\n}: {\n  seed: number;\n  count: number;\n  start: number;\n  drift: number;\n  vol: number;\n  startedAt: number;\n}): Candle[] {\n  const rand = mulberry32(seed);\n  const out: Candle[] = [];\n  let price = start;\n  let volatility = vol;\n\n  for (let i = 0; i < count; i += 1) {\n    volatility += (vol - volatility) * 0.05 + (rand() - 0.5) * vol * 0.35;\n    volatility = Math.max(vol * 0.35, Math.min(vol * 2.6, volatility));\n\n    const open = price;\n    const shock = (rand() - 0.5) * 2 * volatility + drift;\n    const close = Math.max(0.01, open * (1 + shock));\n    const wick = Math.abs(shock) * 0.9 + volatility * 0.55;\n    const high = Math.max(open, close) * (1 + rand() * wick);\n    const low = Math.min(open, close) * (1 - rand() * wick);\n    const range = Math.abs(close - open) / Math.max(open, 1e-6);\n    const volume = Math.round((0.55 + rand() * 0.7 + range * 26) * 1_000_000);\n\n    out.push({\n      t: startedAt + i * DAY_MS,\n      open: round2(open),\n      high: round2(high),\n      low: round2(low),\n      close: round2(close),\n      volume,\n    });\n    price = close;\n  }\n  return out;\n}\n\nexport function round2(v: number) {\n  return Math.round(v * 100) / 100;\n}\n\nconst SERIES_END = Date.UTC(2026, 7, 21);\nconst SERIES_LEN = 420;\nconst SERIES_START = SERIES_END - (SERIES_LEN - 1) * DAY_MS;\n\nexport const SOL_MARKET = generateCandles({\n  seed: 20_260_823,\n  count: SERIES_LEN,\n  start: 96.4,\n  drift: 0.0021,\n  vol: 0.031,\n  startedAt: SERIES_START,\n});\n\nexport const AAPL_MARKET = generateCandles({\n  seed: 7_431_902,\n  count: SERIES_LEN,\n  start: 189.2,\n  drift: 0.0009,\n  vol: 0.013,\n  startedAt: SERIES_START,\n});\n\nexport const BTC_MARKET = generateCandles({\n  seed: 41_556_073,\n  count: SERIES_LEN,\n  start: 61_400,\n  drift: 0.0016,\n  vol: 0.022,\n  startedAt: SERIES_START,\n});\n\nexport function formatMoney(value: number, compact = false) {\n  const digits = compact ? 2 : value >= 1000 ? 2 : value >= 1 ? 2 : 4;\n  return new Intl.NumberFormat('en-US', {\n    style: 'currency',\n    currency: 'USD',\n    notation: compact ? 'compact' : 'standard',\n    minimumFractionDigits: compact ? 0 : digits,\n    maximumFractionDigits: digits,\n  }).format(value);\n}\n\nexport function formatSignedPct(value: number) {\n  return `${value > 0 ? '+' : value < 0 ? '−' : ''}${Math.abs(value).toFixed(2)}%`;\n}\n\nexport const DATE_SHORT = new Intl.DateTimeFormat('en-US', {\n  month: 'short',\n  day: 'numeric',\n  timeZone: 'UTC',\n});\nexport const DATE_FULL = new Intl.DateTimeFormat('en-US', {\n  month: 'short',\n  day: 'numeric',\n  year: 'numeric',\n  timeZone: 'UTC',\n});\n\nexport function formatAxisPrice(value: number) {\n  if (Math.abs(value) >= 10_000) return formatMoney(value, true);\n  return formatMoney(value, false);\n}\n\nexport function niceTicks(lo: number, hi: number, target = 5): number[] {\n  if (!Number.isFinite(lo) || !Number.isFinite(hi) || hi <= lo) return [lo];\n  const raw = (hi - lo) / target;\n  const mag = Math.pow(10, Math.floor(Math.log10(raw)));\n  const norm = raw / mag;\n  const step = (norm >= 7.5 ? 10 : norm >= 3.5 ? 5 : norm >= 2.25 ? 2.5 : norm >= 1.5 ? 2 : 1) * mag;\n  const first = Math.ceil(lo / step) * step;\n  const out: number[] = [];\n  for (let v = first; v <= hi + step * 0.001; v += step) {\n    out.push(Math.round(v / step) * step);\n  }\n  return out;\n}\n\nexport function monotonePath(points: { x: number; y: number }[]): string {\n  const n = points.length;\n  if (n === 0) return '';\n  if (n === 1) return `M${points[0].x},${points[0].y}`;\n\n  const dx: number[] = [];\n  const slope: number[] = [];\n  for (let i = 0; i < n - 1; i += 1) {\n    dx[i] = points[i + 1].x - points[i].x;\n    slope[i] = dx[i] === 0 ? 0 : (points[i + 1].y - points[i].y) / dx[i];\n  }\n\n  const tangent = new Array<number>(n);\n  tangent[0] = slope[0];\n  tangent[n - 1] = slope[n - 2];\n  for (let i = 1; i < n - 1; i += 1) {\n    if (slope[i - 1] * slope[i] <= 0) {\n      tangent[i] = 0;\n    } else {\n      const w1 = 2 * dx[i] + dx[i - 1];\n      const w2 = dx[i] + 2 * dx[i - 1];\n      tangent[i] = (w1 + w2) / (w1 / slope[i - 1] + w2 / slope[i]);\n    }\n  }\n\n  let d = `M${points[0].x},${points[0].y}`;\n  for (let i = 0; i < n - 1; i += 1) {\n    const c1x = points[i].x + dx[i] / 3;\n    const c1y = points[i].y + (tangent[i] * dx[i]) / 3;\n    const c2x = points[i + 1].x - dx[i] / 3;\n    const c2y = points[i + 1].y - (tangent[i + 1] * dx[i]) / 3;\n    d += `C${c1x.toFixed(2)},${c1y.toFixed(2)} ${c2x.toFixed(2)},${c2y.toFixed(2)} ${points[i + 1].x.toFixed(2)},${points[i + 1].y.toFixed(2)}`;\n  }\n  return d;\n}\n\nexport const MORPH_SAMPLES = 132;\n\nexport function resample(values: number[], n = MORPH_SAMPLES): number[] {\n  if (values.length === 0) return new Array(n).fill(0);\n  if (values.length === 1) return new Array(n).fill(values[0]);\n  const out = new Array<number>(n);\n  for (let i = 0; i < n; i += 1) {\n    const p = (i / (n - 1)) * (values.length - 1);\n    const lo = Math.floor(p);\n    const hi = Math.min(values.length - 1, lo + 1);\n    out[i] = values[lo] + (values[hi] - values[lo]) * (p - lo);\n  }\n  return out;\n}\n\nexport function usePrefersReducedMotion() {\n  const [reduce, setReduce] = React.useState(false);\n  React.useEffect(() => {\n    const mq = window.matchMedia('(prefers-reduced-motion: reduce)');\n    const sync = () => setReduce(mq.matches);\n    sync();\n    mq.addEventListener('change', sync);\n    return () => mq.removeEventListener('change', sync);\n  }, []);\n  return reduce;\n}\n\nexport function useElementWidth<T extends HTMLElement>() {\n  const ref = React.useRef<T | null>(null);\n  const [width, setWidth] = React.useState(0);\n  React.useEffect(() => {\n    const node = ref.current;\n    if (!node) return;\n    const ro = new ResizeObserver(([entry]) => {\n      setWidth(entry.contentRect.width);\n    });\n    ro.observe(node);\n    setWidth(node.getBoundingClientRect().width);\n    return () => ro.disconnect();\n  }, []);\n  return [ref, width] as const;\n}\n\nexport const easeOutCubic = (t: number) => 1 - Math.pow(1 - t, 3);\n\nexport function useTween(target: number[], { duration = 520, enabled = true } = {}) {\n  const [value, setValue] = React.useState(target);\n  const currentRef = React.useRef(target);\n  const fromRef = React.useRef(target);\n  const toRef = React.useRef(target);\n  const startRef = React.useRef(0);\n  const rafRef = React.useRef(0);\n\n  React.useEffect(() => {\n    if (!enabled) return;\n    const to = toRef.current;\n    const changed = to.length !== target.length || target.some((v, i) => v !== to[i]);\n    if (!changed) return;\n\n    toRef.current = target;\n\n    if (currentRef.current.length !== target.length) {\n      currentRef.current = target;\n      fromRef.current = target;\n      cancelAnimationFrame(rafRef.current);\n      rafRef.current = requestAnimationFrame(() => setValue(target));\n      return;\n    }\n\n    fromRef.current = currentRef.current;\n    startRef.current = performance.now();\n    cancelAnimationFrame(rafRef.current);\n\n    const tick = (now: number) => {\n      const p = Math.min(1, (now - startRef.current) / duration);\n      const e = easeOutCubic(p);\n      const from = fromRef.current;\n      const dest = toRef.current;\n      const next = dest.map((v, i) => from[i] + (v - from[i]) * e);\n      currentRef.current = next;\n      setValue(next);\n      if (p < 1) rafRef.current = requestAnimationFrame(tick);\n    };\n    rafRef.current = requestAnimationFrame(tick);\n  });\n\n  React.useEffect(() => () => cancelAnimationFrame(rafRef.current), []);\n\n  if (!enabled || value.length !== target.length) return target;\n  return value;\n}\n\nexport function useTweenNumber(target: number, options?: { duration?: number; enabled?: boolean }) {\n  const vec = React.useMemo(() => [target], [target]);\n  return useTween(vec, options)[0];\n}\n\nexport const KEYFRAMES = `\n@keyframes spectrum-mc-rise {\n  from { transform: scaleY(0); opacity: 0; }\n  to   { transform: scaleY(1); opacity: 1; }\n}\n@keyframes spectrum-mc-draw {\n  from { stroke-dashoffset: 1; }\n  to   { stroke-dashoffset: 0; }\n}\n@keyframes spectrum-mc-fade {\n  from { opacity: 0; }\n  to   { opacity: 1; }\n}\n@keyframes spectrum-sk-pulse {\n  from { opacity: 1; }\n  to   { opacity: 0.4; }\n}\n@keyframes spectrum-mc-enter {\n  from { opacity: 0; transform: translateY(6px); }\n  to   { opacity: 1; transform: translateY(0); }\n}\n@keyframes spectrum-mc-grow {\n  from { transform: scaleX(0); }\n  to   { transform: scaleX(1); }\n}\n@keyframes spectrum-mc-flash {\n  from { opacity: 0.2; }\n  to   { opacity: 0; }\n}\n@keyframes spectrum-mc-ping {\n  0%   { r: 4; opacity: 0.55; }\n  70%  { r: 13; opacity: 0; }\n  100% { r: 13; opacity: 0; }\n}\n`;\n\nexport function Keyframes() {\n  return <style>{KEYFRAMES}</style>;\n}\n\nconst DIGITS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];\n\nfunction Digit({ char, animate }: { char: string; animate: boolean }) {\n  const digit = char >= '0' && char <= '9' ? Number(char) : null;\n  if (digit == null) {\n    return (\n      <span aria-hidden className=\"inline-block h-[1em] align-bottom leading-none\">\n        {char}\n      </span>\n    );\n  }\n  return (\n    <span\n      aria-hidden\n      className=\"relative inline-block h-[1em] w-[1ch] overflow-hidden align-bottom leading-none\"\n    >\n      <span\n        className=\"absolute inset-x-0 top-0 block\"\n        style={{\n          transform: `translateY(-${digit}em)`,\n          transition: animate ? 'transform 620ms cubic-bezier(0.22, 1, 0.36, 1)' : undefined,\n        }}\n      >\n        {DIGITS.map((d) => (\n          <span key={d} className=\"block h-[1em] text-center leading-none\">\n            {d}\n          </span>\n        ))}\n      </span>\n    </span>\n  );\n}\n\nexport function RollingNumber({\n  value,\n  format,\n  className,\n  animate = true,\n}: {\n  value: number;\n  format: (value: number) => string;\n  className?: string;\n  animate?: boolean;\n}) {\n  const text = format(value);\n  return (\n    <span className={cn('inline-flex items-end leading-none tabular-nums', className)}>\n      <span className=\"sr-only\">{text}</span>\n      {text.split('').map((char, index) => (\n        <Digit key={`${index}-${char >= '0' && char <= '9' ? 'digit' : char}`} char={char} animate={animate} />\n      ))}\n    </span>\n  );\n}\n\nexport function RangeSelector({\n  ranges,\n  value,\n  onChange,\n  reduce,\n}: {\n  ranges: MarketRange[];\n  value: string;\n  onChange: (label: string) => void;\n  reduce: boolean;\n}) {\n  const index = Math.max(0, ranges.findIndex((r) => r.label === value));\n  const width = 100 / ranges.length;\n\n  return (\n    <div\n      role=\"tablist\"\n      aria-label=\"Time range\"\n      className=\"relative inline-flex items-center rounded-full bg-black/[0.045] p-0.5 dark:bg-white/[0.07]\"\n    >\n      <span\n        aria-hidden\n        className=\"absolute inset-y-0.5 left-0.5 rounded-full bg-white shadow-sm ring-1 ring-black/[0.06] dark:bg-white/12 dark:ring-white/10\"\n        style={{\n          width: `calc(${width}% - 4px)`,\n          transform: `translateX(calc(${index * 100}% + ${index * 4}px))`,\n          transition: reduce ? undefined : 'transform 380ms cubic-bezier(0.22, 1, 0.36, 1)',\n        }}\n      />\n      {ranges.map((range) => {\n        const active = range.label === value;\n        return (\n          <button\n            key={range.label}\n            type=\"button\"\n            role=\"tab\"\n            aria-selected={active}\n            onClick={() => onChange(range.label)}\n            className={cn(\n              'relative z-10 rounded-full px-2.5 py-1 font-mono text-[11px] leading-none tracking-wide transition-colors duration-200',\n              'focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-black/20 dark:focus-visible:ring-white/25',\n              active\n                ? 'text-neutral-950 dark:text-white'\n                : 'text-neutral-500 hover:text-neutral-800 dark:text-neutral-400 dark:hover:text-neutral-200',\n            )}\n            style={{ flex: `0 0 ${width}%` }}\n          >\n            {range.label}\n          </button>\n        );\n      })}\n    </div>\n  );\n}\n\nexport type BookLevel = {\n  price: number;\n  size: number;\n  total: number;\n};\n\nexport type OrderBook = {\n  mid: number;\n  spread: number;\n  bids: BookLevel[];\n  asks: BookLevel[];\n};\n\nexport function generateOrderBook({\n  mid,\n  seed,\n  levels = 14,\n  tick,\n  depth = 900,\n}: {\n  mid: number;\n  seed: number;\n  levels?: number;\n  tick?: number;\n  depth?: number;\n}): OrderBook {\n  const rand = mulberry32(seed);\n  const step = tick ?? Math.max(0.01, round2(mid * 0.0006));\n\n  const side = (direction: 1 | -1): BookLevel[] => {\n    const out: BookLevel[] = [];\n    let total = 0;\n    for (let i = 0; i < levels; i += 1) {\n      const distance = (i + 1) / levels;\n      const base = depth * (0.18 + distance * 0.9);\n      const whale = rand() > 0.87 ? 2.4 : 1;\n      const size = Math.round(base * (0.55 + rand() * 0.9) * whale);\n      total += size;\n      out.push({\n        price: round2(mid + direction * step * (i + 1)),\n        size,\n        total,\n      });\n    }\n    return out;\n  };\n\n  const asks = side(1);\n  const bids = side(-1);\n  return { mid, spread: round2(asks[0].price - bids[0].price), bids, asks };\n}\n\nexport type TreemapInput = { label: string; weight: number; change: number; name?: string };\nexport type TreemapTile = TreemapInput & { x: number; y: number; w: number; h: number };\n\nexport function squarify(\n  items: TreemapInput[],\n  width: number,\n  height: number,\n): TreemapTile[] {\n  const sorted = [...items].filter((i) => i.weight > 0).sort((a, b) => b.weight - a.weight);\n  const totalWeight = sorted.reduce((sum, i) => sum + i.weight, 0);\n  if (!sorted.length || totalWeight <= 0 || width <= 0 || height <= 0) return [];\n\n  const scale = (width * height) / totalWeight;\n  const out: TreemapTile[] = [];\n  let x = 0;\n  let y = 0;\n  let w = width;\n  let h = height;\n  let row: TreemapInput[] = [];\n  let index = 0;\n\n  const worst = (candidate: TreemapInput[], side: number) => {\n    if (!candidate.length || side <= 0) return Infinity;\n    const areas = candidate.map((i) => i.weight * scale);\n    const sum = areas.reduce((a, b) => a + b, 0);\n    const max = Math.max(...areas);\n    const min = Math.min(...areas);\n    const side2 = side * side;\n    const sum2 = sum * sum;\n    return Math.max((side2 * max) / sum2, sum2 / (side2 * min));\n  };\n\n  const layoutRow = (candidate: TreemapInput[], side: number, horizontal: boolean) => {\n    const sum = candidate.reduce((total, i) => total + i.weight * scale, 0);\n    const thickness = sum / side;\n    let offset = 0;\n    for (const item of candidate) {\n      const length = (item.weight * scale) / thickness;\n      out.push(\n        horizontal\n          ? { ...item, x: x + offset, y, w: length, h: thickness }\n          : { ...item, x, y: y + offset, w: thickness, h: length },\n      );\n      offset += length;\n    }\n    if (horizontal) {\n      y += thickness;\n      h -= thickness;\n    } else {\n      x += thickness;\n      w -= thickness;\n    }\n  };\n\n  while (index < sorted.length) {\n    const horizontal = w >= h;\n    const side = horizontal ? w : h;\n    const next = sorted[index];\n\n    if (!row.length || worst([...row, next], side) <= worst(row, side)) {\n      row.push(next);\n      index += 1;\n    } else {\n      layoutRow(row, side, horizontal);\n      row = [];\n    }\n  }\n  if (row.length) layoutRow(row, w >= h ? w : h, w >= h);\n\n  return out;\n}\n\nexport function changeColor(change: number, cap = 4) {\n  const t = Math.max(-1, Math.min(1, change / cap));\n  if (Math.abs(t) < 0.04) return 'var(--spectrum-heat-flat)';\n  const weight = 0.22 + Math.abs(t) * 0.78;\n  const base = t > 0 ? UP : DOWN;\n  return `color-mix(in srgb, ${base} ${(weight * 100).toFixed(0)}%, var(--spectrum-heat-flat))`;\n}\n\nexport const seriesVarsClassName =\n  '[--spectrum-series-1:#2563eb] [--spectrum-series-2:#f59e0b] [--spectrum-series-3:#0d9488] [--spectrum-series-4:#7c3aed] [--spectrum-series-5:#db2777] [--spectrum-series-6:#64748b] [--spectrum-track:#ececef] [--spectrum-chart-surface:#fff] [--spectrum-chart-up:#059669] [--spectrum-chart-down:#e11d48] dark:[--spectrum-series-1:#60a5fa] dark:[--spectrum-series-2:#fbbf24] dark:[--spectrum-series-3:#2dd4bf] dark:[--spectrum-series-4:#a78bfa] dark:[--spectrum-series-5:#f472b6] dark:[--spectrum-series-6:#94a3b8] dark:[--spectrum-track:#26262b] dark:[--spectrum-chart-surface:#0a0a0a] dark:[--spectrum-chart-up:#34d399] dark:[--spectrum-chart-down:#fb7185]';\n\nexport const SERIES_COLORS = [\n  'var(--spectrum-series-1)',\n  'var(--spectrum-series-2)',\n  'var(--spectrum-series-3)',\n  'var(--spectrum-series-4)',\n  'var(--spectrum-series-5)',\n  'var(--spectrum-series-6)',\n] as const;\n\nexport const TRACK = 'var(--spectrum-track)';\n\nexport const textHalo = {\n  paintOrder: 'stroke',\n  stroke: SURFACE,\n  strokeWidth: 3.5,\n  strokeLinejoin: 'round',\n} as const satisfies React.CSSProperties;\n\nexport function intensityColor(t: number, hue = 'var(--spectrum-series-1)') {\n  const clamped = Math.max(0, Math.min(1, t));\n  if (clamped <= 0.001) return TRACK;\n  const weight = 14 + clamped * 86;\n  return `color-mix(in srgb, ${hue} ${weight.toFixed(0)}%, ${TRACK})`;\n}\n\nexport function formatCount(value: number, digits = 1) {\n  if (Math.abs(value) < 1000) return String(Math.round(value));\n  return new Intl.NumberFormat('en-US', {\n    notation: 'compact',\n    maximumFractionDigits: digits,\n  }).format(value);\n}\n\nexport function formatPct(value: number, digits = 1) {\n  return `${value.toFixed(digits)}%`;\n}\n\nexport function onFillClass(intensity: number) {\n  return intensity > 0.55\n    ? 'fill-white dark:fill-neutral-950'\n    : 'fill-neutral-900 dark:fill-white';\n}\n\nexport type ChartStatus = 'ready' | 'loading' | 'empty' | 'error';\n\nexport type SkeletonVariant = 'bars' | 'line' | 'grid' | 'rows' | 'arc' | 'cards';\n\nconst SKELETON_BARS = [46, 72, 55, 83, 41, 68, 92, 57, 76, 49, 88, 63];\nconst SKELETON_ROWS = [92, 74, 86, 61, 79, 55];\n\nfunction SkeletonShapes({\n  variant,\n  height,\n  mode,\n  reduce,\n}: {\n  variant: SkeletonVariant;\n  height: number;\n  mode: 'pulse' | 'ghost';\n  reduce: boolean;\n}) {\n  const animate = mode === 'pulse' && !reduce;\n  const breathe = (index: number): React.CSSProperties | undefined =>\n    animate\n      ? { animation: `spectrum-sk-pulse 1.5s ease-in-out ${index * 120}ms infinite alternate` }\n      : undefined;\n  const block = 'rounded-md bg-black/[0.06] dark:bg-white/[0.08]';\n\n  if (variant === 'bars') {\n    return (\n      <div className=\"flex h-full items-end gap-2 pb-5 pt-3\">\n        {SKELETON_BARS.map((h, i) => (\n          <div key={i} className={cn('flex-1', block)} style={{ height: `${h}%`, ...breathe(i) }} />\n        ))}\n      </div>\n    );\n  }\n\n  if (variant === 'line') {\n    return (\n      <div className=\"relative flex h-full flex-col justify-between py-4\">\n        {[0, 1, 2, 3].map((i) => (\n          <div key={i} className=\"h-px w-full bg-black/[0.05] dark:bg-white/[0.06]\" />\n        ))}\n        <svg\n          className=\"absolute inset-x-0 top-1/4 h-1/2 w-full\"\n          preserveAspectRatio=\"none\"\n          viewBox=\"0 0 100 40\"\n        >\n          <path\n            d=\"M0,30 C12,10 22,34 34,22 C46,10 54,28 66,16 C78,6 88,20 100,8\"\n            fill=\"none\"\n            className=\"stroke-black/[0.1] dark:stroke-white/[0.12]\"\n            strokeWidth={2}\n            vectorEffect=\"non-scaling-stroke\"\n            style={breathe(1)}\n          />\n        </svg>\n      </div>\n    );\n  }\n\n  if (variant === 'grid') {\n    return (\n      <div className=\"grid h-full grid-cols-12 grid-rows-6 gap-1.5 py-2\">\n        {Array.from({ length: 72 }, (_, i) => (\n          <div\n            key={i}\n            className={cn('h-full w-full rounded-[3px]', block)}\n            style={breathe(i % 14)}\n          />\n        ))}\n      </div>\n    );\n  }\n\n  if (variant === 'rows') {\n    return (\n      <div className=\"flex h-full flex-col justify-evenly py-2\">\n        {SKELETON_ROWS.map((w, i) => (\n          <div key={i} className={cn('h-4', block)} style={{ width: `${w}%`, ...breathe(i) }} />\n        ))}\n      </div>\n    );\n  }\n\n  if (variant === 'cards') {\n    return (\n      <div className=\"grid h-full grid-cols-2 content-center gap-3 py-2 sm:grid-cols-4\">\n        {[0, 1, 2, 3].map((i) => (\n          <div\n            key={i}\n            className=\"flex h-full min-h-16 flex-col justify-between gap-2 rounded-xl border border-black/[0.05] p-3 dark:border-white/[0.06]\"\n            style={breathe(i)}\n          >\n            <div className={cn('h-2.5 w-1/2', block)} />\n            <div className={cn('h-5 w-3/4', block)} />\n            <div className={cn('h-6 w-full', block)} />\n          </div>\n        ))}\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex h-full items-center justify-center\">\n      <div\n        className=\"rounded-full border-[14px] border-black/[0.06] dark:border-white/[0.08]\"\n        style={{ width: height * 0.6, height: height * 0.6, ...breathe(0) }}\n      />\n    </div>\n  );\n}\n\nexport function ChartSkeleton({\n  variant = 'bars',\n  height = 300,\n  className,\n}: {\n  variant?: SkeletonVariant;\n  height?: number;\n  className?: string;\n}) {\n  const reduce = usePrefersReducedMotion();\n  return (\n    <div\n      className={cn('relative w-full overflow-hidden', className)}\n      style={{ height }}\n      aria-hidden\n    >\n      <Keyframes />\n      <SkeletonShapes variant={variant} height={height} mode=\"pulse\" reduce={reduce} />\n    </div>\n  );\n}\n\nfunction StateShell({\n  height,\n  variant,\n  icon,\n  iconClassName,\n  title,\n  description,\n  action,\n}: {\n  height: number;\n  variant: SkeletonVariant;\n  icon: React.ReactNode;\n  iconClassName?: string;\n  title: string;\n  description?: string;\n  action?: React.ReactNode;\n}) {\n  const reduce = usePrefersReducedMotion();\n  return (\n    <div\n      className=\"relative flex w-full items-center justify-center overflow-hidden rounded-xl border border-black/[0.06] bg-black/[0.015] dark:border-white/[0.08] dark:bg-white/[0.02]\"\n      style={{ height }}\n      role=\"status\"\n    >\n      <Keyframes />\n      <div\n        aria-hidden\n        className=\"pointer-events-none absolute inset-x-5 inset-y-4 opacity-60 dark:opacity-50\"\n        style={{\n          maskImage:\n            'radial-gradient(ellipse 62% 58% at 50% 50%, transparent 34%, black 78%)',\n          WebkitMaskImage:\n            'radial-gradient(ellipse 62% 58% at 50% 50%, transparent 34%, black 78%)',\n        }}\n      >\n        <SkeletonShapes variant={variant} height={height} mode=\"ghost\" reduce={reduce} />\n      </div>\n      <div\n        className=\"relative flex max-w-[38ch] flex-col items-center gap-1 px-6 text-center\"\n        style={reduce ? undefined : { animation: `spectrum-mc-enter 480ms ${EASE} both` }}\n      >\n        <span\n          className={cn(\n            'mb-2 flex size-10 items-center justify-center rounded-xl border border-black/[0.07] bg-white text-neutral-500 shadow-xs dark:border-white/[0.1] dark:bg-neutral-900 dark:text-neutral-400',\n            iconClassName,\n          )}\n        >\n          {icon}\n        </span>\n        <p className=\"text-[13px] font-medium text-neutral-900 dark:text-neutral-100\">{title}</p>\n        {description ? (\n          <p className=\"text-[12px] leading-relaxed text-neutral-500 dark:text-neutral-400\">\n            {description}\n          </p>\n        ) : null}\n        {action ? <div className=\"mt-3\">{action}</div> : null}\n      </div>\n    </div>\n  );\n}\n\nconst actionClass =\n  'inline-flex items-center gap-1.5 rounded-full border border-black/10 bg-white px-3 py-1.5 text-[12px] font-medium text-neutral-900 shadow-xs transition-colors hover:bg-black/[0.03] focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-black/20 dark:border-white/12 dark:bg-white/[0.06] dark:text-white dark:hover:bg-white/[0.1] dark:focus-visible:ring-white/25';\n\nexport function ChartEmpty({\n  height = 300,\n  variant = 'line',\n  title = 'No data yet',\n  description = 'Once events start arriving this chart will fill in automatically.',\n  action,\n}: {\n  height?: number;\n  variant?: SkeletonVariant;\n  title?: string;\n  description?: string;\n  action?: React.ReactNode;\n}) {\n  return (\n    <StateShell\n      height={height}\n      variant={variant}\n      title={title}\n      description={description}\n      action={action}\n      icon={\n        <svg width=\"17\" height=\"17\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden>\n          <path\n            d=\"M4 19h16M7 16V9m5 7V5m5 11v-4\"\n            stroke=\"currentColor\"\n            strokeWidth=\"1.7\"\n            strokeLinecap=\"round\"\n          />\n        </svg>\n      }\n    />\n  );\n}\n\nexport function ChartError({\n  height = 300,\n  variant = 'line',\n  title = 'Could not load this chart',\n  description = 'The request failed. Check the connection and try again.',\n  onRetry,\n  retryLabel = 'Retry',\n}: {\n  height?: number;\n  variant?: SkeletonVariant;\n  title?: string;\n  description?: string;\n  onRetry?: () => void;\n  retryLabel?: string;\n}) {\n  return (\n    <StateShell\n      height={height}\n      variant={variant}\n      title={title}\n      description={description}\n      iconClassName=\"text-rose-500/90 dark:text-rose-400/90\"\n      action={\n        onRetry ? (\n          <button type=\"button\" onClick={onRetry} className={actionClass}>\n            <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden>\n              <path\n                d=\"M20 11A8 8 0 1 0 18 16M20 5v6h-6\"\n                stroke=\"currentColor\"\n                strokeWidth=\"1.9\"\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n              />\n            </svg>\n            {retryLabel}\n          </button>\n        ) : null\n      }\n      icon={\n        <svg width=\"17\" height=\"17\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden>\n          <path\n            d=\"M12 8v5m0 3.5v.5M10.3 3.9 2.6 17.3A2 2 0 0 0 4.3 20h15.4a2 2 0 0 0 1.7-2.7L13.7 3.9a2 2 0 0 0-3.4 0Z\"\n            stroke=\"currentColor\"\n            strokeWidth=\"1.6\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n          />\n        </svg>\n      }\n    />\n  );\n}\n\nexport function ChartState({\n  status = 'ready',\n  height = 300,\n  variant = 'bars',\n  empty,\n  error,\n  onRetry,\n  children,\n}: {\n  status?: ChartStatus;\n  height?: number;\n  variant?: SkeletonVariant;\n  empty?: { title?: string; description?: string; action?: React.ReactNode };\n  error?: { title?: string; description?: string; retryLabel?: string };\n  onRetry?: () => void;\n  children: React.ReactNode;\n}) {\n  if (status === 'loading') {\n    return (\n      <div aria-busy=\"true\" aria-live=\"polite\">\n        <ChartSkeleton variant={variant} height={height} />\n        <span className=\"sr-only\">Loading chart data</span>\n      </div>\n    );\n  }\n  if (status === 'empty') return <ChartEmpty height={height} variant={variant} {...empty} />;\n  if (status === 'error') {\n    return <ChartError height={height} variant={variant} onRetry={onRetry} {...error} />;\n  }\n  return <>{children}</>;\n}\n\nexport function useHoverIndexKeys({\n  count,\n  setIndex,\n  clear,\n}: {\n  count: number;\n  setIndex: React.Dispatch<React.SetStateAction<number | null>>;\n  clear?: () => void;\n}) {\n  return React.useCallback(\n    (event: React.KeyboardEvent) => {\n      const { key } = event;\n      if (key === 'Escape') {\n        event.preventDefault();\n        if (clear) clear();\n        else setIndex(null);\n        return;\n      }\n      if (key === 'Home' || key === 'End') {\n        event.preventDefault();\n        setIndex(key === 'Home' ? 0 : count - 1);\n        return;\n      }\n      if (key !== 'ArrowLeft' && key !== 'ArrowRight') return;\n      event.preventDefault();\n      const step = key === 'ArrowRight' ? 1 : -1;\n      setIndex((current) => {\n        const next = (current ?? count - 1) + step;\n        return Math.max(0, Math.min(count - 1, next));\n      });\n    },\n    [count, setIndex, clear],\n  );\n}\n\nexport function ChartDataTable({\n  caption,\n  columns,\n  rows,\n}: {\n  caption: string;\n  columns: string[];\n  rows: (string | number)[][];\n}) {\n  return (\n    <table className=\"sr-only\">\n      <caption>{caption}</caption>\n      <thead>\n        <tr>\n          {columns.map((column) => (\n            <th key={column} scope=\"col\">\n              {column}\n            </th>\n          ))}\n        </tr>\n      </thead>\n      <tbody>\n        {rows.map((row, index) => (\n          <tr key={index}>\n            {row.map((cell, cellIndex) =>\n              cellIndex === 0 ? (\n                <th key={cellIndex} scope=\"row\">\n                  {cell}\n                </th>\n              ) : (\n                <td key={cellIndex}>{cell}</td>\n              ),\n            )}\n          </tr>\n        ))}\n      </tbody>\n    </table>\n  );\n}\n\nexport function Stat({ ready, children }: { ready: boolean; children: React.ReactNode }) {\n  if (ready) return <>{children}</>;\n  return (\n    <span aria-hidden className=\"text-neutral-300 dark:text-neutral-600\">\n      —\n    </span>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/spectrumui/charts/chart-engine.tsx"
    },
    {
      "path": "app/registry/charts/indicator-chart.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\nimport { cn } from '@/lib/utils';\nimport {\n  type ChartStatus,\n  ChartDataTable,\n  ChartState,\n  AAPL_MARKET,\n  type Candle,\n  DATE_SHORT,\n  DOWN,\n  EASE,\n  Keyframes,\n  MARKET_RANGES,\n  type MarketRange,\n  RangeSelector,\n  SOL_MARKET,\n  SURFACE,\n  UP,\n  formatAxisPrice,\n  formatMoney,\n  formatSignedPct,\n  marketVarsClassName,\n  monotonePath,\n  niceTicks,\n  useElementWidth,\n  useHoverIndexKeys,\n  usePrefersReducedMotion,\n  useTween,\n} from './chart-engine';\n\nexport function rsi(closes: number[], period = 14): (number | null)[] {\n  const out: (number | null)[] = new Array(closes.length).fill(null);\n  if (closes.length <= period) return out;\n\n  let gain = 0;\n  let loss = 0;\n  for (let i = 1; i <= period; i += 1) {\n    const change = closes[i] - closes[i - 1];\n    if (change >= 0) gain += change;\n    else loss -= change;\n  }\n  gain /= period;\n  loss /= period;\n  out[period] = loss === 0 ? 100 : 100 - 100 / (1 + gain / loss);\n\n  for (let i = period + 1; i < closes.length; i += 1) {\n    const change = closes[i] - closes[i - 1];\n    gain = (gain * (period - 1) + Math.max(0, change)) / period;\n    loss = (loss * (period - 1) + Math.max(0, -change)) / period;\n    out[i] = loss === 0 ? 100 : 100 - 100 / (1 + gain / loss);\n  }\n  return out;\n}\n\nfunction ema(values: number[], period: number): number[] {\n  const k = 2 / (period + 1);\n  const out: number[] = [];\n  let prev = values[0] ?? 0;\n  for (let i = 0; i < values.length; i += 1) {\n    prev = i === 0 ? values[0] : values[i] * k + prev * (1 - k);\n    out.push(prev);\n  }\n  return out;\n}\n\nexport function macd(closes: number[], fast = 12, slow = 26, signalPeriod = 9) {\n  const fastEma = ema(closes, fast);\n  const slowEma = ema(closes, slow);\n  const line = closes.map((_, i) => fastEma[i] - slowEma[i]);\n  const signal = ema(line, signalPeriod);\n  return { line, signal, histogram: line.map((v, i) => v - signal[i]) };\n}\n\nconst PAD = { top: 12, right: 58, bottom: 22, left: 12 };\nconst GAP = 14;\nconst PANE_SHARE = { price: 0.54, rsi: 0.22, macd: 0.24 };\n\nexport interface IndicatorChartProps {\n  className?: string;\n  data?: Candle[];\n  symbol?: string;\n  name?: string;\n  ranges?: MarketRange[];\n  defaultRange?: string;\n  height?: number;\n  status?: ChartStatus;\n  onRetry?: () => void;\n}\n\nexport function IndicatorChart({\n  className,\n  data = SOL_MARKET,\n  symbol = 'SOL',\n  name = 'Solana',\n  ranges = MARKET_RANGES,\n  defaultRange = '3M',\n  height = 460,\n  status = 'ready',\n  onRetry,\n}: IndicatorChartProps) {\n  const reduce = usePrefersReducedMotion();\n  const [wrapRef, width] = useElementWidth<HTMLDivElement>();\n  const svgRef = React.useRef<SVGSVGElement | null>(null);\n  const [rangeLabel, setRangeLabel] = React.useState(\n    () => ranges.find((r) => r.label === defaultRange)?.label ?? ranges[ranges.length - 1].label,\n  );\n  const [hoverIndex, setHoverIndex] = React.useState<number | null>(null);\n\n  const range = ranges.find((r) => r.label === rangeLabel) ?? ranges[ranges.length - 1];\n\n  const closesAll = React.useMemo(() => data.map((c) => c.close), [data]);\n  const rsiAll = React.useMemo(() => rsi(closesAll), [closesAll]);\n  const macdAll = React.useMemo(() => macd(closesAll), [closesAll]);\n\n  const start = React.useMemo(() => {\n    const bars = range.bars;\n    if (bars == null || bars >= data.length) return 0;\n    return data.length - bars;\n  }, [data.length, range.bars]);\n\n  const view = React.useMemo(() => data.slice(start), [data, start]);\n  const rsiView = React.useMemo(() => rsiAll.slice(start), [rsiAll, start]);\n  const macdView = React.useMemo(\n    () => ({\n      line: macdAll.line.slice(start),\n      signal: macdAll.signal.slice(start),\n      histogram: macdAll.histogram.slice(start),\n    }),\n    [macdAll, start],\n  );\n\n  const n = view.length;\n\n  const w = Math.max(width, 260);\n  const h = height;\n  const x0 = PAD.left;\n  const x1 = w - PAD.right;\n  const plotW = Math.max(1, x1 - x0);\n  const innerTop = PAD.top;\n  const innerBottom = h - PAD.bottom;\n  const usable = innerBottom - innerTop - GAP * 2;\n\n  const priceTop = innerTop;\n  const priceBottom = priceTop + usable * PANE_SHARE.price;\n  const rsiTop = priceBottom + GAP;\n  const rsiBottom = rsiTop + usable * PANE_SHARE.rsi;\n  const macdTop = rsiBottom + GAP;\n  const macdBottom = macdTop + usable * PANE_SHARE.macd;\n\n  const step = plotW / Math.max(n, 1);\n  const cx = React.useCallback((i: number) => x0 + step * (i + 0.5), [x0, step]);\n  const barW = Math.max(1, Math.min(step * 0.62, 12));\n\n  const rawPriceDomain = React.useMemo(() => {\n    if (!n) return [0, 1] as const;\n    let lo = Infinity;\n    let hi = -Infinity;\n    for (const c of view) {\n      if (c.low < lo) lo = c.low;\n      if (c.high > hi) hi = c.high;\n    }\n    const pad = (hi - lo) * 0.1 || 1;\n    return [lo - pad, hi + pad] as const;\n  }, [view, n]);\n\n  const [pLo, pHi] = useTween([rawPriceDomain[0], rawPriceDomain[1]], {\n    duration: 520,\n    enabled: !reduce,\n  });\n  const priceY = React.useCallback(\n    (v: number) => priceBottom - ((v - pLo) / (pHi - pLo || 1)) * (priceBottom - priceTop),\n    [pLo, pHi, priceBottom, priceTop],\n  );\n\n  const rsiY = React.useCallback(\n    (v: number) => rsiBottom - (v / 100) * (rsiBottom - rsiTop),\n    [rsiBottom, rsiTop],\n  );\n\n  const macdMax = React.useMemo(() => {\n    let max = 1e-6;\n    for (let i = 0; i < macdView.line.length; i += 1) {\n      max = Math.max(\n        max,\n        Math.abs(macdView.line[i]),\n        Math.abs(macdView.signal[i]),\n        Math.abs(macdView.histogram[i]),\n      );\n    }\n    return max * 1.15;\n  }, [macdView]);\n\n  const macdZero = (macdTop + macdBottom) / 2;\n  const macdY = React.useCallback(\n    (v: number) => macdZero - (v / macdMax) * ((macdBottom - macdTop) / 2),\n    [macdZero, macdMax, macdBottom, macdTop],\n  );\n\n  const closeLine = React.useMemo(\n    () => monotonePath(view.map((c, i) => ({ x: cx(i), y: priceY(c.close) }))),\n    [view, cx, priceY],\n  );\n  const rsiLine = React.useMemo(() => {\n    const points = rsiView\n      .map((v, i) => (v == null ? null : { x: cx(i), y: rsiY(v) }))\n      .filter((p): p is { x: number; y: number } => p != null);\n    return monotonePath(points);\n  }, [rsiView, cx, rsiY]);\n  const macdLine = React.useMemo(\n    () => monotonePath(macdView.line.map((v, i) => ({ x: cx(i), y: macdY(v) }))),\n    [macdView.line, cx, macdY],\n  );\n  const signalLine = React.useMemo(\n    () => monotonePath(macdView.signal.map((v, i) => ({ x: cx(i), y: macdY(v) }))),\n    [macdView.signal, cx, macdY],\n  );\n\n  const priceTicks = React.useMemo(() => niceTicks(pLo, pHi, 4), [pLo, pHi]);\n  const timeTicks = React.useMemo(() => {\n    if (!n) return [];\n    const want = Math.max(2, Math.min(6, Math.floor(plotW / 96)));\n    const gap = Math.max(1, Math.floor((n - 1) / (want - 1 || 1)));\n    const out: number[] = [];\n    for (let i = 0; i < n; i += gap) out.push(i);\n    const minGap = 62;\n    if (out[out.length - 1] !== n - 1) {\n      while (out.length && cx(n - 1) - cx(out[out.length - 1]) < minGap) out.pop();\n      out.push(n - 1);\n    }\n    return out;\n  }, [n, plotW, cx]);\n\n  const onKeyDown = useHoverIndexKeys({ count: n, setIndex: setHoverIndex });\n\n  const onMove = (clientX: number) => {\n    const svg = svgRef.current;\n    if (!svg) return;\n    const box = svg.getBoundingClientRect();\n    const x = ((clientX - box.left) / box.width) * w;\n    setHoverIndex(Math.max(0, Math.min(n - 1, Math.floor((x - x0) / step))));\n  };\n\n  const last = view[n - 1];\n  const activeIndex = hoverIndex ?? n - 1;\n  const active = view[activeIndex];\n  const activeRsi = rsiView[activeIndex];\n  const activeMacd = macdView.line[activeIndex];\n  const activeSignal = macdView.signal[activeIndex];\n  const base = view[0]?.close ?? 0;\n  const delta = base ? (((active?.close ?? 0) - base) / base) * 100 : 0;\n  const rangeDelta = base ? (((last?.close ?? 0) - base) / base) * 100 : 0;\n  const readoutColor = delta >= 0 ? UP : DOWN;\n  const dirColor = rangeDelta >= 0 ? UP : DOWN;\n\n  const uid = React.useId().replace(/:/g, '');\n  const ready = width > 0;\n  const introKey = rangeLabel;\n\n  const paneLabel = (text: string, y: number) => (\n    <text\n      x={x0 + 2}\n      y={y + 10}\n      fontSize={9.5}\n      fontWeight={600}\n      fill=\"currentColor\"\n      opacity={0.55}\n      className=\"font-mono uppercase tracking-wider\"\n    >\n      {text}\n    </text>\n  );\n\n  return (\n    <div\n      ref={wrapRef}\n      className={cn(\n        'flex w-full flex-col text-neutral-400 dark:text-neutral-500',\n        marketVarsClassName,\n        className,\n      )}\n    >\n      <Keyframes />\n\n      <div className=\"mb-3 flex flex-wrap items-start justify-between gap-x-4 gap-y-3\">\n        <div>\n          <div className=\"flex items-baseline gap-2\">\n            <span className=\"font-mono text-[12px] font-medium tracking-wide text-neutral-950 dark:text-white\">\n              {symbol}\n            </span>\n            <span className=\"text-[12px] text-neutral-500 dark:text-neutral-400\">{name}</span>\n          </div>\n          <div className=\"mt-1 flex items-end gap-2.5\">\n            <span className=\"font-mono text-[24px] font-medium leading-none tabular-nums text-neutral-950 dark:text-white\">\n              {formatMoney(active?.close ?? 0)}\n            </span>\n            <span className=\"mb-0.5 font-mono text-[12px] tabular-nums\" style={{ color: readoutColor }}>\n              {formatSignedPct(delta)}\n            </span>\n          </div>\n          <div className=\"mt-1.5 flex flex-wrap items-center gap-x-3 font-mono text-[10.5px] tabular-nums\">\n            <span className=\"text-neutral-400 dark:text-neutral-500\">\n              {active ? DATE_SHORT.format(active.t) : ''}\n            </span>\n            <span className=\"text-neutral-500 dark:text-neutral-400\">\n              RSI{' '}\n              <span\n                className=\"text-neutral-950 dark:text-white\"\n                style={{\n                  color:\n                    activeRsi == null ? undefined : activeRsi >= 70 ? DOWN : activeRsi <= 30 ? UP : undefined,\n                }}\n              >\n                {activeRsi == null ? '—' : activeRsi.toFixed(1)}\n              </span>\n            </span>\n            <span className=\"text-neutral-500 dark:text-neutral-400\">\n              MACD{' '}\n              <span className=\"text-neutral-950 dark:text-white\">\n                {activeMacd == null ? '—' : activeMacd.toFixed(2)}\n              </span>\n            </span>\n            <span className=\"text-neutral-500 dark:text-neutral-400\">\n              Signal{' '}\n              <span className=\"text-neutral-950 dark:text-white\">\n                {activeSignal == null ? '—' : activeSignal.toFixed(2)}\n              </span>\n            </span>\n          </div>\n        </div>\n\n        <RangeSelector\n          ranges={ranges}\n          value={rangeLabel}\n          onChange={setRangeLabel}\n          reduce={reduce}\n        />\n      </div>\n\n      <ChartState\n\n        status={status}\n\n        height={height}\n\n        variant=\"line\"\n\n        empty={{ title: 'No price data', description: 'Indicators need at least 26 candles before MACD can be computed.' }}\n\n        onRetry={onRetry}\n\n      >\n\n      <div className=\"relative w-full\" style={{ height }}>\n        {!ready ? null : (\n          <svg\n            ref={svgRef}\n            width={w}\n            height={h}\n            viewBox={`0 0 ${w} ${h}`}\n            className=\"block w-full touch-pan-y select-none overflow-visible\"\n            role=\"img\"\n            aria-label={`${symbol} ${name} with RSI and MACD, ${rangeLabel} range. ${formatMoney(last?.close ?? 0)}, ${formatSignedPct(rangeDelta)}.`}\n            tabIndex={0}\n            onKeyDown={onKeyDown}\n            onBlur={() => setHoverIndex(null)}\n            onPointerMove={(e) => onMove(e.clientX)}\n            onPointerDown={(e) => onMove(e.clientX)}\n            onPointerLeave={() => setHoverIndex(null)}\n          >\n            <defs>\n              <linearGradient id={`${uid}-price`} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n                <stop offset=\"0%\" stopColor={dirColor} stopOpacity={0.24} />\n                <stop offset=\"100%\" stopColor={dirColor} stopOpacity={0} />\n              </linearGradient>\n              <clipPath id={`${uid}-rsi`}>\n                <rect x={x0} y={rsiTop} width={plotW} height={Math.max(0, rsiBottom - rsiTop)} />\n              </clipPath>\n            </defs>\n\n            <g shapeRendering=\"crispEdges\">\n              {priceTicks.map((tick) => {\n                const y = priceY(tick);\n                if (y < priceTop - 1 || y > priceBottom + 1) return null;\n                if (last && Math.abs(y - priceY(last.close)) < 13) return null;\n                return (\n                  <line\n                    key={tick}\n                    x1={x0}\n                    x2={x1}\n                    y1={y}\n                    y2={y}\n                    stroke=\"currentColor\"\n                    strokeOpacity={0.12}\n                    strokeDasharray=\"2 4\"\n                  />\n                );\n              })}\n            </g>\n            <g className=\"font-mono\">\n              {priceTicks.map((tick) => {\n                const y = priceY(tick);\n                if (y < priceTop - 1 || y > priceBottom + 1) return null;\n                if (last && Math.abs(y - priceY(last.close)) < 13) return null;\n                return (\n                  <text\n                    key={tick}\n                    x={x1 + 8}\n                    y={y}\n                    dominantBaseline=\"middle\"\n                    fontSize={10.5}\n                    fill=\"currentColor\"\n                    className=\"tabular-nums\"\n                  >\n                    {formatAxisPrice(tick)}\n                  </text>\n                );\n              })}\n            </g>\n\n            <g key={`${introKey}-price`}>\n              <path\n                d={`${closeLine}L${cx(n - 1)},${priceBottom}L${cx(0)},${priceBottom}Z`}\n                fill={`url(#${uid}-price)`}\n                style={reduce ? undefined : { animation: `spectrum-mc-fade 560ms ease-out both` }}\n              />\n              <path\n                d={closeLine}\n                fill=\"none\"\n                stroke={dirColor}\n                strokeWidth={2}\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n                pathLength={1}\n                style={\n                  reduce\n                    ? undefined\n                    : { strokeDasharray: 1, animation: `spectrum-mc-draw 880ms ${EASE} both` }\n                }\n              />\n            </g>\n            {paneLabel(`${symbol} · close`, priceTop - 10)}\n\n            {last ? (\n              <g>\n                <line\n                  x1={x0}\n                  x2={x1}\n                  y1={priceY(last.close)}\n                  y2={priceY(last.close)}\n                  stroke={dirColor}\n                  strokeOpacity={0.5}\n                  strokeDasharray=\"4 4\"\n                />\n                <rect x={x1 + 3} y={priceY(last.close) - 9} width={PAD.right - 8} height={18} rx={4} fill={dirColor} />\n                <text\n                  x={x1 + 3 + (PAD.right - 8) / 2}\n                  y={priceY(last.close)}\n                  textAnchor=\"middle\"\n                  dominantBaseline=\"middle\"\n                  fontSize={10.5}\n                  fontWeight={600}\n                  fill={SURFACE}\n                  className=\"font-mono tabular-nums\"\n                >\n                  {formatAxisPrice(last.close)}\n                </text>\n              </g>\n            ) : null}\n\n            <g clipPath={`url(#${uid}-rsi)`}>\n              <rect\n                x={x0}\n                y={rsiY(70)}\n                width={plotW}\n                height={Math.max(0, rsiY(30) - rsiY(70))}\n                fill=\"currentColor\"\n                opacity={0.05}\n              />\n              {[30, 70].map((level) => (\n                <line\n                  key={level}\n                  x1={x0}\n                  x2={x1}\n                  y1={rsiY(level)}\n                  y2={rsiY(level)}\n                  stroke={level === 70 ? DOWN : UP}\n                  strokeOpacity={0.45}\n                  strokeDasharray=\"3 3\"\n                />\n              ))}\n              <path\n                key={`${introKey}-rsi`}\n                d={rsiLine}\n                fill=\"none\"\n                stroke=\"var(--spectrum-chart-4, #7c3aed)\"\n                strokeWidth={1.6}\n                strokeLinecap=\"round\"\n                pathLength={1}\n                style={\n                  reduce\n                    ? undefined\n                    : { strokeDasharray: 1, animation: `spectrum-mc-draw 880ms ${EASE} 120ms both` }\n                }\n              />\n            </g>\n            <g className=\"font-mono\">\n              {[30, 70].map((level) => (\n                <text\n                  key={level}\n                  x={x1 + 8}\n                  y={rsiY(level)}\n                  dominantBaseline=\"middle\"\n                  fontSize={9.5}\n                  fill=\"currentColor\"\n                  className=\"tabular-nums\"\n                >\n                  {level}\n                </text>\n              ))}\n            </g>\n            {paneLabel('RSI 14', rsiTop - 10)}\n\n            <g key={`${introKey}-macd`}>\n              <line\n                x1={x0}\n                x2={x1}\n                y1={macdZero}\n                y2={macdZero}\n                stroke=\"currentColor\"\n                strokeOpacity={0.2}\n                shapeRendering=\"crispEdges\"\n              />\n              {macdView.histogram.map((v, i) => {\n                const y = macdY(v);\n                const positive = v >= 0;\n                return (\n                  <rect\n                    key={i}\n                    x={cx(i) - barW / 2}\n                    y={positive ? y : macdZero}\n                    width={barW}\n                    height={Math.max(0.75, Math.abs(macdZero - y))}\n                    rx={1}\n                    fill={positive ? UP : DOWN}\n                    opacity={hoverIndex != null && hoverIndex !== i ? 0.22 : 0.5}\n                    style={{\n                      transition: reduce ? undefined : 'opacity 160ms ease-out',\n                      transformBox: 'fill-box',\n                      transformOrigin: positive ? 'bottom center' : 'top center',\n                      animation: reduce\n                        ? undefined\n                        : `spectrum-mc-rise 420ms ${EASE} ${n <= 1 ? 0 : (i / (n - 1)) * 300}ms both`,\n                    }}\n                  />\n                );\n              })}\n              <path d={macdLine} fill=\"none\" stroke=\"var(--spectrum-chart-1, #2563eb)\" strokeWidth={1.5} />\n              <path\n                d={signalLine}\n                fill=\"none\"\n                stroke=\"var(--spectrum-chart-2, #f59e0b)\"\n                strokeWidth={1.5}\n                strokeDasharray=\"4 3\"\n              />\n            </g>\n            {paneLabel('MACD 12 · 26 · 9', macdTop - 10)}\n\n            <g className=\"font-mono\">\n              {timeTicks.map((i) => {\n                const candle = view[i];\n                if (!candle) return null;\n                return (\n                  <text\n                    key={i}\n                    x={Math.max(x0 + 14, Math.min(x1 - 14, cx(i)))}\n                    y={innerBottom + 14}\n                    textAnchor=\"middle\"\n                    fontSize={10.5}\n                    fill=\"currentColor\"\n                    className=\"tabular-nums\"\n                  >\n                    {DATE_SHORT.format(candle.t)}\n                  </text>\n                );\n              })}\n            </g>\n\n            {hoverIndex != null && view[hoverIndex] ? (\n              <g>\n                <line\n                  x1={cx(hoverIndex)}\n                  x2={cx(hoverIndex)}\n                  y1={priceTop}\n                  y2={macdBottom}\n                  stroke=\"currentColor\"\n                  strokeOpacity={0.42}\n                  strokeDasharray=\"3 3\"\n                />\n                <circle\n                  cx={cx(hoverIndex)}\n                  cy={priceY(view[hoverIndex].close)}\n                  r={4}\n                  fill={SURFACE}\n                  stroke={dirColor}\n                  strokeWidth={2}\n                />\n                {rsiView[hoverIndex] != null ? (\n                  <circle\n                    cx={cx(hoverIndex)}\n                    cy={rsiY(rsiView[hoverIndex] as number)}\n                    r={3}\n                    fill={SURFACE}\n                    stroke=\"var(--spectrum-chart-4, #7c3aed)\"\n                    strokeWidth={1.75}\n                  />\n                ) : null}\n                <circle\n                  cx={cx(hoverIndex)}\n                  cy={macdY(macdView.line[hoverIndex])}\n                  r={3}\n                  fill={SURFACE}\n                  stroke=\"var(--spectrum-chart-1, #2563eb)\"\n                  strokeWidth={1.75}\n                />\n                <g\n                  transform={`translate(${Math.max(x0 + 28, Math.min(x1 - 28, cx(hoverIndex)))}, ${innerBottom + 4})`}\n                >\n                  <rect x={-27} y={0} width={54} height={16} rx={4} className=\"fill-neutral-900 dark:fill-white\" />\n                  <text\n                    x={0}\n                    y={8.5}\n                    textAnchor=\"middle\"\n                    dominantBaseline=\"middle\"\n                    fontSize={10}\n                    fontWeight={600}\n                    className=\"fill-white font-mono tabular-nums dark:fill-neutral-950\"\n                  >\n                    {DATE_SHORT.format(view[hoverIndex].t)}\n                  </text>\n                </g>\n              </g>\n            ) : null}\n          </svg>\n        )}\n      </div>\n      </ChartState>\n      <ChartDataTable\n        caption={`${symbol} ${name} — close, RSI and MACD by date`}\n        columns={['Date', 'Close', 'RSI', 'MACD']}\n        rows={view.map((c, i) => [DATE_SHORT.format(c.t), formatMoney(c.close), rsiView[i] == null ? '—' : (rsiView[i] as number).toFixed(1), macdView.line[i].toFixed(2)])}\n      />\n    </div>\n  );\n}\n\nexport function DefaultIndicatorChart(props: IndicatorChartProps) {\n  return <IndicatorChart {...props} />;\n}\n\nexport function StockIndicatorChart(props: IndicatorChartProps) {\n  return <IndicatorChart data={AAPL_MARKET} symbol=\"AAPL\" name=\"Apple Inc.\" {...props} />;\n}\n",
      "type": "registry:component",
      "target": "components/spectrumui/charts/indicator-chart.tsx"
    }
  ],
  "type": "registry:component"
}
