{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "candlestick-chart",
  "type": "registry:component",
  "title": "Candlestick Chart",
  "description": "OHLC financial chart for session-by-session price action.",
  "dependencies": [
    "@vllnt/ui@^0.2.1"
  ],
  "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 { 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  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.value}>\n      <line\n        stroke=\"hsl(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=\"hsl(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 fill = isBullish ? \"hsl(142 71% 45%)\" : \"hsl(348 83% 47%)\";\n\n    return (\n      <g key={candle.label}>\n        <line\n          stroke={fill}\n          strokeLinecap=\"round\"\n          strokeWidth={2}\n          x1={centerX}\n          x2={centerX}\n          y1={wickTop}\n          y2={wickBottom}\n        />\n        <rect\n          fill={fill}\n          fillOpacity={isBullish ? 0.25 : 0.18}\n          height={bodyHeight}\n          rx={4}\n          stroke={fill}\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=\"hsl(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 = React.forwardRef<\n  HTMLDivElement,\n  CandlestickChartProps\n>(\n  (\n    {\n      className,\n      data,\n      height = DEFAULT_HEIGHT,\n      showGrid = true,\n      width = DEFAULT_WIDTH,\n      ...props\n    },\n    reference,\n  ) => {\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            <h3 className=\"text-lg font-semibold text-foreground\">\n              Candlestick chart\n            </h3>\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);\n\nCandlestickChart.displayName = \"CandlestickChart\";\n",
      "type": "registry:component"
    }
  ],
  "version": "0.2.1",
  "stability": "stable"
}
