{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "author": "Lopifi (https://lopifi.com)",
  "categories": [
    "swaps-fx",
    "finance",
    "accounting"
  ],
  "dependencies": [],
  "description": "Rate history with a period selector.",
  "files": [
    {
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { useInViewOnce } from \"@/hooks/ledger/use-in-view\";\nimport { useReducedMotion } from \"@/hooks/ledger/use-media-query\";\nimport { niceTicks } from \"@/lib/ledger/chart-utils\";\nimport { cn } from \"@/lib/ledger/cn\";\nimport { AsOf } from \"@/components/ledger/as-of\";\n\nexport const CHART_PERIODS = [\"1D\", \"1W\", \"1M\", \"3M\", \"1Y\"] as const;\nexport type ChartPeriod = (typeof CHART_PERIODS)[number];\n\nexport interface RateSeries {\n  labels: string[];\n  /** Rate history, oldest first. */\n  rates: number[];\n}\n\nexport interface CurrencyPairChartProps {\n  asOf: Date;\n  className?: string;\n  /** Falls back to 1M, then to the longest horizon supplied. */\n  defaultPeriod?: ChartPeriod;\n  from: string;\n  /**\n   * One entry per horizon you actually hold history for. The selector renders\n   * exactly these and nothing else: a period button that redraws the same\n   * line is a control that lies about the data behind it, and one horizon of\n   * history is a chart, not a chart with a broken selector.\n   */\n  series: Partial<Record<ChartPeriod, RateSeries>>;\n  to: string;\n}\n\n/**\n * The Y-axis does not start at zero. It is a rate, not a quantity, and\n * forcing zero destroys the resolution — this is the one sanctioned\n * exception to the zero-baseline rule, and the axis says so.\n *\n * Draws once on mount, instant on period change.\n */\nexport function CurrencyPairChart({\n  from,\n  to,\n  series,\n  defaultPeriod,\n  asOf,\n  className,\n}: CurrencyPairChartProps) {\n  /* Two points are a line segment, not a history. */\n  const offered = CHART_PERIODS.filter((p) => (series[p]?.rates.length ?? 0) > 1);\n  const initial =\n    (defaultPeriod && offered.includes(defaultPeriod) ? defaultPeriod : undefined) ??\n    (offered.includes(\"1M\") ? \"1M\" : offered.at(-1));\n  const [period, setPeriod] = React.useState<ChartPeriod | undefined>(initial);\n  const reduced = useReducedMotion();\n  const [_viewRef, inView] = useInViewOnce<SVGSVGElement>();\n\n  const active = (period ? series[period] : undefined) ?? { labels: [], rates: [] };\n  const { rates, labels } = active;\n  if (rates.length < 2) return null;\n\n  const width = 720;\n  const height = 220;\n  const PAD = { bottom: 28, left: 72, right: 16, top: 16 };\n\n  const min = Math.min(...rates);\n  const max = Math.max(...rates);\n  const pad = (max - min) * 0.15 || 0.001;\n  const lo = min - pad;\n  const hi = max + pad;\n  const ticks = niceTicks(lo, hi, 4);\n\n  const plotW = width - PAD.left - PAD.right;\n  const plotH = height - PAD.top - PAD.bottom;\n  const x = (i: number) => PAD.left + (i / (rates.length - 1)) * plotW;\n  const y = (v: number) => PAD.top + plotH - ((v - lo) / (hi - lo || 1)) * plotH;\n  const path = rates.map((v, i) => `${i === 0 ? \"M\" : \"L\"}${x(i)},${y(v)}`).join(\" \");\n\n  return (\n    <div className={cn(\"w-full\", className)}>\n      <div className=\"flex flex-wrap items-center justify-between gap-3\">\n        <div>\n          <p className=\"text-label font-medium\">\n            {from}/{to}\n          </p>\n          <AsOf date={asOf} />\n        </div>\n        {/* One horizon is not a choice, and a single pressed button that does\n            nothing is worse than no button at all. */}\n        {offered.length > 1 ? (\n          <div aria-label=\"Chart period\" className=\"flex gap-1\" role=\"group\">\n            {offered.map((p) => (\n              <button\n                aria-pressed={period === p}\n                className={cn(\n                  \"h-8 rounded-sm px-2.5 text-label font-medium\",\n                  \"transition-colors duration-1 ease-standard\",\n                  period === p\n                    ? \"bg-surface-selected text-text-primary\"\n                    : \"text-text-secondary hover:bg-surface-hover\",\n                )}\n                key={p}\n                onClick={() => setPeriod(p)}\n                type=\"button\"\n              >\n                {p}\n              </button>\n            ))}\n          </div>\n        ) : null}\n      </div>\n\n      <svg\n        aria-label={`${from} to ${to} rate history`}\n        className=\"mt-3 w-full\"\n        role=\"img\"\n        style={{ height }}\n        viewBox={`0 0 ${width} ${height}`}\n      >\n        {ticks.map((t) => (\n          <g key={t}>\n            <line stroke=\"var(--border-subtle)\" x1={PAD.left} x2={width - PAD.right} y1={y(t)} y2={y(t)} />\n            <text\n              className=\"numeric\"\n              fill=\"var(--text-tertiary)\"\n              fontSize=\"12\"\n              textAnchor=\"end\"\n              x={PAD.left - 8}\n              y={y(t) + 4}\n            >\n              {t.toFixed(4)}\n            </text>\n          </g>\n        ))}\n        <path\n          d={path}\n          fill=\"none\"\n          stroke=\"var(--viz-1)\"\n          strokeLinejoin=\"round\"\n          strokeWidth=\"2\"\n          style={\n            reduced\n              ? undefined\n              : {\n                  animation: inView ? \"ledger-draw 400ms var(--ease-out) forwards\" : undefined,\n                  strokeDasharray: 2000,\n                  strokeDashoffset: 2000,\n                }\n          }\n        />\n        {labels.map((label, i) =>\n          i % Math.ceil(labels.length / 6) === 0 ? (\n            /* Labels repeat across horizons — \"Mon\" twice in a week — so the\n               index is part of the key. */\n            <text\n              className=\"numeric\"\n              fill=\"var(--text-tertiary)\"\n              fontSize=\"12\"\n              key={`${label}-${i}`}\n              textAnchor=\"middle\"\n              x={x(i)}\n              y={height - 8}\n            >\n              {label}\n            </text>\n          ) : null,\n        )}\n      </svg>\n      <p className=\"text-caption text-text-tertiary\">\n        Axis does not start at zero — this is a rate, not a quantity.\n      </p>\n    </div>\n  );\n}\n",
      "path": "components/ledger/currency-pair-chart.tsx",
      "target": "components/ledger/currency-pair-chart.tsx",
      "type": "registry:component"
    }
  ],
  "name": "currency-pair-chart",
  "registryDependencies": [
    "@lopifi/theme",
    "@lopifi/as-of",
    "@lopifi/chart-utils",
    "@lopifi/cn",
    "@lopifi/use-in-view",
    "@lopifi/use-media-query"
  ],
  "title": "CurrencyPairChart",
  "type": "registry:component"
}
