{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "candlestick-chart",
  "title": "Candlestick Chart",
  "description": "OHLC financial chart for session-by-session price action.",
  "dependencies": [
    "@vllnt/ui@^0.3.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/candlestick-chart/candlestick-chart.tsx",
      "content": "import * as React from \"react\";\n\nimport { ArrowDownRight, ArrowUpRight } from \"lucide-react\";\n\nimport type { HeadingTag } from \"@vllnt/ui\";\nimport { cn } from \"@vllnt/ui\";\n\nexport type CandlestickDatum = {\n  close: number;\n  high: number;\n  label: string;\n  low: number;\n  open: number;\n};\n\nexport type CandlestickChartProps = {\n  /** Heading tag for the title. Defaults to `h3`. */\n  as?: HeadingTag;\n  data: CandlestickDatum[];\n  height?: number;\n  showGrid?: boolean;\n  width?: number;\n} & React.HTMLAttributes<HTMLDivElement>;\n\ntype ChartMetrics = {\n  bodyWidth: number;\n  bottomPadding: number;\n  chartHeight: number;\n  columnWidth: number;\n  maxPrice: number;\n  minPrice: number;\n  range: number;\n  topPadding: number;\n};\n\nconst DEFAULT_WIDTH = 760;\nconst DEFAULT_HEIGHT = 260;\n\nfunction formatValue(value: number) {\n  return value.toLocaleString(undefined, {\n    maximumFractionDigits: 2,\n    minimumFractionDigits: 2,\n  });\n}\n\nfunction buildMetrics(\n  data: CandlestickDatum[],\n  height: number,\n  width: number,\n): ChartMetrics {\n  const allValues = data.flatMap((candle) => [\n    candle.high,\n    candle.low,\n    candle.open,\n    candle.close,\n  ]);\n  const minPrice = Math.min(...allValues);\n  const maxPrice = Math.max(...allValues);\n  const range = maxPrice - minPrice || 1;\n  const topPadding = 20;\n  const bottomPadding = 30;\n  const chartHeight = height - topPadding - bottomPadding;\n  const columnWidth = width / data.length;\n  const bodyWidth = Math.max(columnWidth * 0.56, 8);\n\n  return {\n    bodyWidth,\n    bottomPadding,\n    chartHeight,\n    columnWidth,\n    maxPrice,\n    minPrice,\n    range,\n    topPadding,\n  };\n}\n\nfunction getYForPrice(price: number, metrics: ChartMetrics) {\n  const ratio = (price - metrics.minPrice) / metrics.range;\n  return metrics.topPadding + metrics.chartHeight - ratio * metrics.chartHeight;\n}\n\nfunction PriceGrid({\n  metrics,\n  showGrid,\n  width,\n}: {\n  metrics: ChartMetrics;\n  showGrid: boolean;\n  width: number;\n}) {\n  if (!showGrid) {\n    return null;\n  }\n\n  const ticks = Array.from({ length: 4 }, (_, index) => {\n    const ratio = index / 3;\n    return {\n      value: metrics.maxPrice - ratio * metrics.range,\n      y: metrics.topPadding + ratio * metrics.chartHeight,\n    };\n  });\n\n  return ticks.map((tick) => (\n    <g key={tick.y}>\n      <line\n        stroke=\"oklch(var(--border))\"\n        strokeDasharray=\"4 6\"\n        strokeOpacity=\"0.8\"\n        x1=\"0\"\n        x2={width}\n        y1={tick.y}\n        y2={tick.y}\n      />\n      <text\n        fill=\"oklch(var(--muted-foreground))\"\n        fontSize=\"11\"\n        textAnchor=\"end\"\n        x={width - 6}\n        y={tick.y - 4}\n      >\n        {formatValue(tick.value)}\n      </text>\n    </g>\n  ));\n}\n\nfunction CandleMarks({\n  data,\n  height,\n  metrics,\n}: {\n  data: CandlestickDatum[];\n  height: number;\n  metrics: ChartMetrics;\n}) {\n  return data.map((candle, index) => {\n    const centerX = metrics.columnWidth * index + metrics.columnWidth / 2;\n    const wickTop = getYForPrice(candle.high, metrics);\n    const wickBottom = getYForPrice(candle.low, metrics);\n    const openY = getYForPrice(candle.open, metrics);\n    const closeY = getYForPrice(candle.close, metrics);\n    const bodyY = Math.min(openY, closeY);\n    const bodyHeight = Math.max(Math.abs(openY - closeY), 3);\n    const isBullish = candle.close >= candle.open;\n    const colorClass = isBullish\n      ? \"text-emerald-600 dark:text-emerald-400\"\n      : \"text-rose-600 dark:text-rose-400\";\n\n    return (\n      <g className={colorClass} key={`${candle.label}-${index.toString()}`}>\n        <line\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeWidth={2}\n          x1={centerX}\n          x2={centerX}\n          y1={wickTop}\n          y2={wickBottom}\n        />\n        <rect\n          fill=\"currentColor\"\n          fillOpacity={isBullish ? 0.25 : 0.18}\n          height={bodyHeight}\n          rx={4}\n          stroke=\"currentColor\"\n          strokeWidth={1.5}\n          width={metrics.bodyWidth}\n          x={centerX - metrics.bodyWidth / 2}\n          y={bodyY}\n        >\n          <title>\n            {`${candle.label}: O ${formatValue(candle.open)} H ${formatValue(candle.high)} L ${formatValue(candle.low)} C ${formatValue(candle.close)}`}\n          </title>\n        </rect>\n        <text\n          fill=\"oklch(var(--muted-foreground))\"\n          fontSize=\"11\"\n          textAnchor=\"middle\"\n          x={centerX}\n          y={height - 8}\n        >\n          {candle.label}\n        </text>\n      </g>\n    );\n  });\n}\n\nfunction SessionPill({ sessionChange }: { sessionChange: number }) {\n  const isPositive = sessionChange >= 0;\n  const TrendIcon = isPositive ? ArrowUpRight : ArrowDownRight;\n\n  return (\n    <div\n      className={cn(\n        \"inline-flex items-center gap-2 rounded-full border px-3 py-1 text-sm font-medium\",\n        isPositive\n          ? \"border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400\"\n          : \"border-rose-500/30 bg-rose-500/10 text-rose-600 dark:text-rose-400\",\n      )}\n    >\n      <TrendIcon className=\"size-4\" />\n      {sessionChange >= 0 ? \"+\" : \"\"}\n      {formatValue(sessionChange)}\n    </div>\n  );\n}\n\nexport const CandlestickChart = ({\n  as: Heading = \"h3\",\n  className,\n  data,\n  height = DEFAULT_HEIGHT,\n  ref: reference,\n  showGrid = true,\n  width = DEFAULT_WIDTH,\n  ...props\n}: CandlestickChartProps & { ref?: React.Ref<HTMLDivElement> }) => {\n  const firstCandle = data[0];\n  const finalCandle = data.at(-1);\n\n  if (!firstCandle || !finalCandle) {\n    return null;\n  }\n\n  const metrics = buildMetrics(data, height, width);\n  const sessionChange = finalCandle.close - firstCandle.open;\n\n  return (\n    <div\n      className={cn(\n        \"rounded-2xl border border-border bg-card/80 p-4 shadow-sm\",\n        className,\n      )}\n      ref={reference}\n      {...props}\n    >\n      <div className=\"mb-4 flex flex-wrap items-start justify-between gap-3\">\n        <div>\n          <p className=\"text-xs font-medium uppercase tracking-[0.28em] text-muted-foreground\">\n            OHLC session\n          </p>\n          <Heading className=\"text-lg font-semibold text-foreground\">\n            Candlestick chart\n          </Heading>\n        </div>\n        <SessionPill sessionChange={sessionChange} />\n      </div>\n      <svg\n        aria-label=\"Candlestick chart\"\n        className=\"h-full w-full\"\n        height={height}\n        role=\"img\"\n        viewBox={`0 0 ${width} ${height}`}\n        width={width}\n      >\n        <PriceGrid metrics={metrics} showGrid={showGrid} width={width} />\n        <CandleMarks data={data} height={height} metrics={metrics} />\n      </svg>\n    </div>\n  );\n};\n\nCandlestickChart.displayName = \"CandlestickChart\";\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component",
  "version": "0.3.0",
  "stability": "stable"
}
