{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "candlestick-chart",
  "type": "registry:ui",
  "title": "Candlestick Chart",
  "description": "An interactive OHLC chart with volume, candle pinning, time ranges, and price-change measurements.",
  "dependencies": [
    "gsap",
    "@gsap/react",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/registry/ui/candlestick-chart.tsx",
      "content": "\"use client\";\n\nimport React, {\n  forwardRef,\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport gsap from \"gsap\";\nimport { useGSAP } from \"@gsap/react\";\nimport { cn } from \"@/lib/utils\";\n\ngsap.registerPlugin(useGSAP);\n\nexport interface Candle {\n  date: string;\n  open: number;\n  high: number;\n  low: number;\n  close: number;\n  volume?: number;\n}\n\nexport interface CandlestickRange {\n  label: string;\n  sessions?: number;\n}\n\nexport interface CandlestickChartProps extends Omit<\n  React.HTMLAttributes<HTMLDivElement>,\n  \"title\"\n> {\n  data?: Candle[];\n  title?: string;\n  description?: string;\n  valuePrefix?: string;\n  showVolume?: boolean;\n  ranges?: CandlestickRange[];\n  animated?: boolean;\n  onCandleSelect?: (candle: Candle | null) => void;\n}\n\ninterface Measurement {\n  from: number;\n  to: number;\n}\n\nconst AXIS_WIDTH = 48;\nconst AXIS_HEIGHT = 22;\nconst PLOT_TOP = 8;\nconst VOLUME_RATIO = 0.2;\nconst VOLUME_GAP = 8;\nconst PRICE_TICKS = 4;\nconst MAX_DATE_TICKS = 5;\nconst DATE_LABEL_SPACING = 64;\n\nconst DEFAULT_RANGES: CandlestickRange[] = [\n  { label: \"1W\", sessions: 7 },\n  { label: \"2W\", sessions: 14 },\n  { label: \"1M\", sessions: 30 },\n  { label: \"All\" },\n];\n\nconst roundToCents = (value: number) => Math.round(value * 100) / 100;\n\nfunction getNiceStep(rawStep: number) {\n  const magnitude = 10 ** Math.floor(Math.log10(rawStep));\n  const normalized = rawStep / magnitude;\n  const factor =\n    normalized <= 1\n      ? 1\n      : normalized <= 2\n        ? 2\n        : normalized <= 2.5\n          ? 2.5\n          : normalized <= 5\n            ? 5\n            : 10;\n  return factor * magnitude;\n}\n\nfunction generateDemoCandles(count = 40): Candle[] {\n  let seed = 7;\n  const random = () => {\n    seed = (seed * 9301 + 49297) % 233280;\n    return seed / 233280;\n  };\n  const candles: Candle[] = [];\n  let price = 182;\n\n  for (let index = 0; index < count; index++) {\n    const open = price;\n    const close = Math.max(\n      1,\n      open + (random() - 0.48) * 6 + Math.sin(index / 5) * 1.6,\n    );\n    candles.push({\n      date: new Date(Date.UTC(2026, 7, 1 + index)).toISOString().slice(0, 10),\n      open: roundToCents(open),\n      high: roundToCents(Math.max(open, close) + random() * 2.6),\n      low: roundToCents(Math.min(open, close) - random() * 2.6),\n      close: roundToCents(close),\n      volume: Math.round(1_200_000 + random() * 2_600_000),\n    });\n    price = close;\n  }\n\n  return candles;\n}\n\nconst DEMO_CANDLES = generateDemoCandles();\n\nconst priceFormatter = new Intl.NumberFormat(\"en-US\", {\n  minimumFractionDigits: 2,\n  maximumFractionDigits: 2,\n});\n\nconst dateFormatter = new Intl.DateTimeFormat(\"en-US\", {\n  month: \"short\",\n  day: \"numeric\",\n  timeZone: \"UTC\",\n});\n\nconst formatDate = (date: string) => {\n  const parsed = new Date(date);\n  return Number.isNaN(parsed.getTime()) ? date : dateFormatter.format(parsed);\n};\n\nconst formatPercent = (value: number) =>\n  Number.isFinite(value) ? `${value >= 0 ? \"+\" : \"\"}${value.toFixed(2)}%` : \"N/A\";\n\nconst isValidCandle = (candle: Candle) =>\n  [candle.open, candle.high, candle.low, candle.close].every(Number.isFinite) &&\n  candle.low <= Math.min(candle.open, candle.close) &&\n  candle.high >= Math.max(candle.open, candle.close) &&\n  (candle.volume === undefined ||\n    (Number.isFinite(candle.volume) && candle.volume >= 0));\n\nconst rangeButtonClass =\n  \"relative z-10 w-9 rounded-md py-1 text-[11px] font-medium text-zinc-500 transition-colors duration-150 hover:text-zinc-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 aria-pressed:text-zinc-900 motion-reduce:transition-none dark:text-zinc-400 dark:hover:text-zinc-100 dark:aria-pressed:text-zinc-100\";\n\nexport const CandlestickChart = forwardRef<\n  HTMLDivElement,\n  CandlestickChartProps\n>(function CandlestickChart(\n  {\n    data = DEMO_CANDLES,\n    title = \"Price action\",\n    description,\n    valuePrefix = \"$\",\n    showVolume = true,\n    ranges = DEFAULT_RANGES,\n    animated = true,\n    onCandleSelect,\n    className,\n    ...props\n  },\n  forwardedRef,\n) {\n  const containerRef = useRef<HTMLDivElement | null>(null);\n  const plotRef = useRef<HTMLDivElement | null>(null);\n  const dragRef = useRef<{ anchor: number; last: number } | null>(null);\n  const [size, setSize] = useState({ width: 0, height: 0 });\n  const [rangeLabel, setRangeLabel] = useState(ranges.at(-1)?.label);\n  const [storedHoverIndex, setHoverIndex] = useState<number | null>(null);\n  const [storedPinnedIndex, setPinnedIndex] = useState<number | null>(null);\n  const [storedMeasurement, setMeasurement] = useState<Measurement | null>(null);\n  const [announcement, setAnnouncement] = useState(\"\");\n\n  const setRefs = useCallback(\n    (node: HTMLDivElement | null) => {\n      containerRef.current = node;\n      if (typeof forwardedRef === \"function\") forwardedRef(node);\n      else if (forwardedRef) forwardedRef.current = node;\n    },\n    [forwardedRef],\n  );\n\n  const validCandles = useMemo(() => data.filter(isValidCandle), [data]);\n  const rangeIndex = ranges.findIndex((range) => range.label === rangeLabel);\n  const sessions = ranges[rangeIndex]?.sessions;\n  const start = sessions !== undefined && Number.isFinite(sessions)\n    ? Math.max(0, validCandles.length - Math.max(1, Math.floor(sessions)))\n    : 0;\n  const candles = useMemo(\n    () => validCandles.slice(start),\n    [validCandles, start],\n  );\n\n  const candlesKey = useMemo(() => JSON.stringify(candles.map(\n    ({ date, open, high, low, close, volume }) =>\n      [date, open, high, low, close, volume ?? null],\n  )), [candles]);\n  const [selectionKey, setSelectionKey] = useState(candlesKey);\n  const hoverIndex = selectionKey === candlesKey ? storedHoverIndex : null;\n  const pinnedIndex = selectionKey === candlesKey ? storedPinnedIndex : null;\n  const measurement = selectionKey === candlesKey ? storedMeasurement : null;\n  const previousSelection = useRef({ candlesKey, pinnedIndex });\n\n  if (selectionKey !== candlesKey) {\n    setSelectionKey(candlesKey);\n    setHoverIndex(null);\n    setPinnedIndex(null);\n    setMeasurement(null);\n    setAnnouncement(\"\");\n  }\n\n  useEffect(() => {\n    const previous = previousSelection.current;\n    if (previous.candlesKey !== candlesKey) {\n      dragRef.current = null;\n      if (previous.pinnedIndex !== null) onCandleSelect?.(null);\n    }\n    previousSelection.current = { candlesKey, pinnedIndex };\n  }, [candlesKey, pinnedIndex, onCandleSelect]);\n\n  const hasSize = size.width > AXIS_WIDTH && size.height > AXIS_HEIGHT + PLOT_TOP;\n  const hasVolume =\n    showVolume && candles.some((candle) => (candle.volume ?? 0) > 0);\n\n  useEffect(() => {\n    const node = plotRef.current;\n    if (!node) return;\n    const observer = new ResizeObserver(([entry]) => {\n      const { width, height } = entry.contentRect;\n      setSize({ width: Math.floor(width), height: Math.floor(height) });\n    });\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, []);\n\n  useGSAP(\n    () => {\n      if (!animated || !hasSize || candles.length === 0) return;\n      const mm = gsap.matchMedia();\n      mm.add(\"(prefers-reduced-motion: no-preference)\", () => {\n        gsap.from(\".candle-grid\", { opacity: 0, duration: 0.4 });\n        gsap.from(\".candle-body\", {\n          scaleY: 0,\n          transformOrigin: \"50% 50%\",\n          duration: 0.55,\n          ease: \"power3.out\",\n          stagger: Math.min(0.018, 0.6 / candles.length),\n        });\n        gsap.from(\".candle-volume\", {\n          scaleY: 0,\n          transformOrigin: \"50% 100%\",\n          duration: 0.5,\n          ease: \"power2.out\",\n          stagger: Math.min(0.018, 0.6 / candles.length),\n          delay: 0.1,\n        });\n      }, containerRef);\n      return () => mm.revert();\n    },\n    {\n      scope: containerRef,\n      dependencies: [candlesKey, animated, hasSize, hasVolume],\n      revertOnUpdate: true,\n    },\n  );\n\n  const layout = useMemo(() => {\n    if (!hasSize || candles.length === 0) return null;\n    const right = size.width - AXIS_WIDTH;\n    const bottom = size.height - AXIS_HEIGHT;\n    const volumeHeight = hasVolume ? (bottom - PLOT_TOP) * VOLUME_RATIO : 0;\n    const priceBottom = hasVolume ? bottom - volumeHeight - VOLUME_GAP : bottom;\n    const rawMin = Math.min(...candles.map((candle) => candle.low));\n    const rawMax = Math.max(...candles.map((candle) => candle.high));\n    const padding = (rawMax - rawMin) * 0.08 || 1;\n    const min = rawMin - padding;\n    const max = rawMax + padding;\n    const step = right / candles.length;\n    const tickStep = getNiceStep((max - min) / PRICE_TICKS);\n    const dateTickCount = Math.max(\n      2,\n      Math.min(MAX_DATE_TICKS, Math.floor(right / DATE_LABEL_SPACING)),\n    );\n\n    return {\n      right,\n      bottom,\n      priceBottom,\n      volumeHeight,\n      step,\n      bodyWidth: Math.max(1, Math.min(14, step * 0.62)),\n      maxVolume: Math.max(...candles.map((candle) => candle.volume ?? 0)),\n      x: (index: number) => step * (index + 0.5),\n      y: (value: number) =>\n        PLOT_TOP + ((max - value) / (max - min)) * (priceBottom - PLOT_TOP),\n      ticks: Array.from(\n        { length: Math.floor(max / tickStep) - Math.ceil(min / tickStep) + 1 },\n        (_, index) => (Math.ceil(min / tickStep) + index) * tickStep,\n      ),\n      tickFormatter: new Intl.NumberFormat(\"en-US\", {\n        minimumFractionDigits: tickStep < 1 ? 2 : 0,\n        maximumFractionDigits: tickStep < 1 ? 2 : 0,\n      }),\n      dateTicks: [\n        ...new Set(\n          Array.from({ length: dateTickCount }, (_, index) =>\n            Math.round((index * (candles.length - 1)) / (dateTickCount - 1)),\n          ),\n        ),\n      ],\n    };\n  }, [candles, hasSize, hasVolume, size.height, size.width]);\n\n  const lastIndex = candles.length - 1;\n  const cursorIndex = hoverIndex ?? pinnedIndex;\n  const displayIndex = cursorIndex ?? lastIndex;\n  const displayCandle = candles[displayIndex];\n  const previousClose =\n    displayIndex > 0 ? candles[displayIndex - 1].close : displayCandle?.open;\n  const change = displayCandle ? displayCandle.close - previousClose : 0;\n  const changePercent = previousClose ? (change / previousClose) * 100 : NaN;\n\n  const formatPrice = (value: number) =>\n    `${valuePrefix}${priceFormatter.format(value)}`;\n\n  const clampIndex = (index: number) => Math.max(0, Math.min(lastIndex, index));\n\n  const describeCandle = (candle: Candle) =>\n    `${formatDate(candle.date)}: open ${formatPrice(candle.open)}, high ${formatPrice(candle.high)}, low ${formatPrice(candle.low)}, close ${formatPrice(candle.close)}`;\n\n  const describeMeasurement = ({ from, to }: Measurement) => {\n    const startCandle = candles[Math.min(from, to)];\n    const endCandle = candles[Math.max(from, to)];\n    const percent = startCandle.close\n      ? ((endCandle.close - startCandle.close) / startCandle.close) * 100\n      : NaN;\n    const span = Math.abs(to - from);\n    return {\n      percent,\n      label: `${formatPercent(percent)} · ${span} ${span === 1 ? \"bar\" : \"bars\"}`,\n      text: `${formatPercent(percent)} from ${formatDate(startCandle.date)} to ${formatDate(endCandle.date)}, ${span} ${span === 1 ? \"session\" : \"sessions\"}`,\n    };\n  };\n\n  const togglePin = (index: number) => {\n    const next = pinnedIndex === index ? null : index;\n    setPinnedIndex(next);\n    previousSelection.current.pinnedIndex = next;\n    onCandleSelect?.(next === null ? null : candles[next]);\n    setAnnouncement(\n      next === null\n        ? \"Selection cleared\"\n        : `Pinned ${describeCandle(candles[next])}`,\n    );\n  };\n\n  const clearSelection = () => {\n    setHoverIndex(null);\n    setMeasurement(null);\n    if (pinnedIndex !== null) {\n      previousSelection.current.pinnedIndex = null;\n      onCandleSelect?.(null);\n    }\n    setPinnedIndex(null);\n  };\n\n  const selectRange = (label: string) => {\n    if (label === rangeLabel) return;\n    clearSelection();\n    setRangeLabel(label);\n  };\n\n  const indexAt = (event: React.PointerEvent<HTMLDivElement>) => {\n    const bounds = event.currentTarget.getBoundingClientRect();\n    return clampIndex(\n      Math.floor((event.clientX - bounds.left) / (layout?.step ?? 1)),\n    );\n  };\n\n  const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {\n    if (!layout || !event.isPrimary || event.button !== 0) return;\n    const index = indexAt(event);\n    event.currentTarget.focus({ preventScroll: true });\n    event.currentTarget.setPointerCapture(event.pointerId);\n    dragRef.current = { anchor: index, last: index };\n    setHoverIndex(index);\n  };\n\n  const handlePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {\n    if (!layout) return;\n    const index = indexAt(event);\n    setHoverIndex(index);\n    const drag = dragRef.current;\n    if (drag && index !== drag.last) {\n      drag.last = index;\n      setMeasurement({ from: drag.anchor, to: index });\n    }\n  };\n\n  const handlePointerUp = () => {\n    const drag = dragRef.current;\n    dragRef.current = null;\n    if (!drag) return;\n    if (drag.last !== drag.anchor) {\n      setAnnouncement(\n        describeMeasurement({ from: drag.anchor, to: drag.last }).text,\n      );\n      return;\n    }\n    setMeasurement(null);\n    togglePin(drag.anchor);\n  };\n\n  const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n    if (candles.length === 0) return;\n    const current = cursorIndex ?? lastIndex;\n\n    if (event.key === \"Escape\") {\n      clearSelection();\n      setAnnouncement(\"Selection cleared\");\n      return;\n    }\n    if (event.key === \"Enter\" || event.key === \" \") {\n      event.preventDefault();\n      togglePin(current);\n      return;\n    }\n\n    const targets: Record<string, number> = {\n      ArrowLeft: cursorIndex === null ? lastIndex : current - 1,\n      ArrowRight: current + 1,\n      Home: 0,\n      End: lastIndex,\n    };\n    if (!(event.key in targets)) return;\n    event.preventDefault();\n    const target = clampIndex(targets[event.key]);\n    setHoverIndex(target);\n\n    if (event.shiftKey) {\n      const next = { from: measurement?.from ?? current, to: target };\n      setMeasurement(next);\n      setAnnouncement(describeMeasurement(next).text);\n      return;\n    }\n    setMeasurement(null);\n    setAnnouncement(describeCandle(candles[target]));\n  };\n\n  const cursorCandle = cursorIndex === null ? null : candles[cursorIndex];\n  const lastCandle = candles[lastIndex];\n  const tagPrice = cursorCandle?.close ?? lastCandle?.close;\n  const tagY = layout && tagPrice !== undefined ? layout.y(tagPrice) : null;\n  const cursorX = layout && cursorIndex !== null ? layout.x(cursorIndex) : null;\n  const lastIsUp = lastCandle ? lastCandle.close >= lastCandle.open : true;\n  const measureLabelWidth = measurement\n    ? describeMeasurement(measurement).label.length * 6.4 + 12\n    : 0;\n  const measureView =\n    measurement && layout\n      ? {\n          ...describeMeasurement(measurement),\n          fromX: layout.x(measurement.from),\n          toX: layout.x(measurement.to),\n          fromY: layout.y(candles[measurement.from].close),\n          toY: layout.y(candles[measurement.to].close),\n          left: layout.x(Math.min(measurement.from, measurement.to)),\n          right: layout.x(Math.max(measurement.from, measurement.to)),\n          labelX: Math.max(\n            measureLabelWidth / 2,\n            Math.min(\n              layout.right - measureLabelWidth / 2,\n              (layout.x(measurement.from) + layout.x(measurement.to)) / 2,\n            ),\n          ),\n        }\n      : null;\n\n  return (\n    <div\n      ref={setRefs}\n      className={cn(\n        \"relative w-full max-w-full sm:max-w-3xl rounded-2xl border border-zinc-200 bg-white/90 p-3 sm:p-6 backdrop-blur-xl shadow-[0_20px_48px_-10px_rgba(0,0,0,0.06),inset_0_1px_0_0_rgba(255,255,255,0.8)] dark:border-white/10 dark:bg-zinc-950/75 dark:shadow-[0_20px_48px_-10px_rgba(0,0,0,0.7),inset_0_1px_0_0_rgba(255,255,255,0.14)] select-none\",\n        className,\n      )}\n      {...props}\n    >\n      <div className=\"flex items-start justify-between gap-3\">\n        <div className=\"flex min-w-0 flex-col\">\n          <span className=\"truncate text-xs sm:text-sm font-semibold tracking-tight text-zinc-900 dark:text-zinc-100\">\n            {title}\n          </span>\n          <span className=\"truncate text-[11px] sm:text-xs text-zinc-500 dark:text-zinc-400\">\n            {[\n              description,\n              displayCandle && formatDate(displayCandle.date),\n              pinnedIndex !== null && hoverIndex === null && \"Pinned\",\n            ]\n              .filter(Boolean)\n              .join(\" · \")}\n          </span>\n        </div>\n        {displayCandle && (\n          <div className=\"flex shrink-0 flex-col items-end\">\n            <span className=\"text-lg sm:text-2xl font-semibold tabular-nums tracking-tight text-zinc-900 dark:text-zinc-100\">\n              {formatPrice(displayCandle.close)}\n            </span>\n            <span\n              className={cn(\n                \"text-xs font-medium tabular-nums\",\n                change >= 0\n                  ? \"text-emerald-600 dark:text-emerald-400\"\n                  : \"text-rose-600 dark:text-rose-400\",\n              )}\n            >\n              {formatPercent(changePercent)}\n            </span>\n          </div>\n        )}\n      </div>\n\n      {ranges.length > 1 && (\n        <div\n          role=\"group\"\n          aria-label=\"Time range\"\n          className=\"relative mt-4 flex w-fit rounded-lg bg-zinc-100 p-0.5 dark:bg-white/5\"\n        >\n          {rangeIndex >= 0 && (\n            <span\n              aria-hidden=\"true\"\n              style={{ transform: `translateX(${rangeIndex * 100}%)` }}\n              className=\"absolute bottom-0.5 left-0.5 top-0.5 w-9 rounded-md bg-white shadow-[0_1px_2px_rgba(0,0,0,0.08)] transition-transform duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] motion-reduce:transition-none dark:bg-white/10\"\n            />\n          )}\n          {ranges.map((range) => (\n            <button\n              key={range.label}\n              type=\"button\"\n              aria-pressed={range.label === rangeLabel}\n              onClick={() => selectRange(range.label)}\n              className={rangeButtonClass}\n            >\n              {range.label}\n            </button>\n          ))}\n        </div>\n      )}\n\n      <div\n        ref={plotRef}\n        tabIndex={candles.length > 0 ? 0 : -1}\n        role=\"group\"\n        aria-roledescription=\"candlestick chart\"\n        aria-label={`${title}, ${candles.length} sessions. Arrow keys inspect candles, Enter pins one, Shift with arrow keys measures change, Escape clears.`}\n        onPointerDown={handlePointerDown}\n        onPointerMove={handlePointerMove}\n        onPointerUp={handlePointerUp}\n        onPointerCancel={() => (dragRef.current = null)}\n        onPointerLeave={() => setHoverIndex(null)}\n        onKeyDown={handleKeyDown}\n        onBlur={() => setHoverIndex(null)}\n        className=\"relative mt-3 h-56 sm:h-64 w-full cursor-crosshair touch-pan-y rounded-lg outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-zinc-500 dark:focus-visible:ring-offset-zinc-950\"\n      >\n        {candles.length === 0 ? (\n          <div className=\"flex h-full items-center justify-center text-xs text-zinc-500 dark:text-zinc-400\">\n            No price data\n          </div>\n        ) : (\n          layout && (\n            <svg\n              width={size.width}\n              height={size.height}\n              className=\"absolute inset-0 overflow-visible\"\n              aria-hidden=\"true\"\n            >\n              <g className=\"candle-grid\">\n                {layout.ticks.map((tick) => (\n                  <g\n                    key={tick}\n                    className={cn(\n                      tagY !== null &&\n                        Math.abs(layout.y(tick) - tagY) < 12 &&\n                        \"[&>text]:opacity-0\",\n                    )}\n                  >\n                    <line\n                      x1={0}\n                      x2={layout.right}\n                      y1={layout.y(tick)}\n                      y2={layout.y(tick)}\n                      className=\"stroke-zinc-100 dark:stroke-white/5\"\n                    />\n                    <text\n                      x={layout.right + 8}\n                      y={layout.y(tick) + 3}\n                      className=\"fill-zinc-400 text-[10px] tabular-nums dark:fill-zinc-500\"\n                    >\n                      {layout.tickFormatter.format(tick)}\n                    </text>\n                  </g>\n                ))}\n                {layout.dateTicks.map((index) => (\n                  <text\n                    key={index}\n                    x={index === 0 ? 0 : layout.x(index)}\n                    y={layout.bottom + 16}\n                    textAnchor={index === 0 ? \"start\" : \"middle\"}\n                    className=\"fill-zinc-400 text-[10px] dark:fill-zinc-500\"\n                  >\n                    {formatDate(candles[index].date)}\n                  </text>\n                ))}\n              </g>\n\n              {measureView && (\n                <rect\n                  x={measureView.left - layout.step / 2}\n                  y={PLOT_TOP}\n                  width={measureView.right - measureView.left + layout.step}\n                  height={layout.priceBottom - PLOT_TOP}\n                  className={\n                    measureView.percent >= 0\n                      ? \"fill-emerald-500/10\"\n                      : \"fill-rose-500/10\"\n                  }\n                />\n              )}\n\n              {candles.map((candle, index) => {\n                const up = candle.close >= candle.open;\n                const x = layout.x(index);\n                const top = layout.y(Math.max(candle.open, candle.close));\n                const bodyHeight = Math.max(\n                  1,\n                  layout.y(Math.min(candle.open, candle.close)) - top,\n                );\n                const volumeHeight = layout.maxVolume\n                  ? ((candle.volume ?? 0) / layout.maxVolume) *\n                    layout.volumeHeight\n                  : 0;\n                const inMeasure =\n                  measurement &&\n                  index >= Math.min(measurement.from, measurement.to) &&\n                  index <= Math.max(measurement.from, measurement.to);\n                const dimmed = measurement\n                  ? !inMeasure\n                  : cursorIndex !== null && cursorIndex !== index;\n\n                return (\n                  <g\n                    key={`${candle.date}-${index}`}\n                    className={cn(\n                      \"transition-opacity duration-150 motion-reduce:transition-none\",\n                      up\n                        ? \"fill-emerald-500 stroke-emerald-500 dark:fill-emerald-400 dark:stroke-emerald-400\"\n                        : \"fill-rose-500 stroke-rose-500 dark:fill-rose-400 dark:stroke-rose-400\",\n                      dimmed ? \"opacity-35\" : \"opacity-100\",\n                    )}\n                  >\n                    {hasVolume && (\n                      <rect\n                        className=\"candle-volume\"\n                        x={x - layout.bodyWidth / 2}\n                        y={layout.bottom - volumeHeight}\n                        width={layout.bodyWidth}\n                        height={volumeHeight}\n                        rx={1}\n                        fillOpacity={0.2}\n                        stroke=\"none\"\n                      />\n                    )}\n                    <g className=\"candle-body\">\n                      <line\n                        x1={x}\n                        x2={x}\n                        y1={layout.y(candle.high)}\n                        y2={layout.y(candle.low)}\n                        strokeWidth={1}\n                      />\n                      <rect\n                        x={x - layout.bodyWidth / 2}\n                        y={top}\n                        width={layout.bodyWidth}\n                        height={bodyHeight}\n                        rx={Math.min(2, layout.bodyWidth / 4)}\n                        stroke=\"none\"\n                      />\n                    </g>\n                  </g>\n                );\n              })}\n\n              {measureView && (\n                <g className=\"pointer-events-none\">\n                  <line\n                    x1={measureView.fromX}\n                    x2={measureView.toX}\n                    y1={measureView.fromY}\n                    y2={measureView.toY}\n                    strokeWidth={1.5}\n                    strokeDasharray=\"4 3\"\n                    className=\"stroke-zinc-900 dark:stroke-zinc-100\"\n                  />\n                  <circle\n                    cx={measureView.fromX}\n                    cy={measureView.fromY}\n                    r={3}\n                    className=\"fill-zinc-900 dark:fill-zinc-100\"\n                  />\n                  <rect\n                    x={measureView.labelX - measureLabelWidth / 2}\n                    y={PLOT_TOP}\n                    width={measureLabelWidth}\n                    height={18}\n                    rx={9}\n                    className={\n                      measureView.percent >= 0\n                        ? \"fill-emerald-500 dark:fill-emerald-400\"\n                        : \"fill-rose-500 dark:fill-rose-400\"\n                    }\n                  />\n                  <text\n                    x={measureView.labelX}\n                    y={PLOT_TOP + 12.5}\n                    textAnchor=\"middle\"\n                    className=\"fill-white text-[10px] font-semibold tabular-nums dark:fill-zinc-950\"\n                  >\n                    {measureView.label}\n                  </text>\n                </g>\n              )}\n\n              {cursorX !== null && (\n                <line\n                  x1={cursorX}\n                  x2={cursorX}\n                  y1={PLOT_TOP}\n                  y2={layout.bottom}\n                  className=\"pointer-events-none stroke-zinc-300 dark:stroke-zinc-600\"\n                  strokeDasharray={\n                    hoverIndex === null && pinnedIndex !== null\n                      ? undefined\n                      : \"3 3\"\n                  }\n                />\n              )}\n\n              {tagY !== null && tagPrice !== undefined && (\n                <g className=\"pointer-events-none\">\n                  <rect\n                    x={layout.right + 2}\n                    y={tagY - 9}\n                    width={AXIS_WIDTH - 4}\n                    height={18}\n                    rx={4}\n                    className={\n                      cursorCandle\n                        ? \"fill-zinc-900 dark:fill-zinc-100\"\n                        : lastIsUp\n                          ? \"fill-emerald-500 dark:fill-emerald-400\"\n                          : \"fill-rose-500 dark:fill-rose-400\"\n                    }\n                  />\n                  <text\n                    x={layout.right + AXIS_WIDTH / 2}\n                    y={tagY + 3.5}\n                    textAnchor=\"middle\"\n                    className={cn(\n                      \"text-[10px] font-medium tabular-nums\",\n                      cursorCandle\n                        ? \"fill-white dark:fill-zinc-900\"\n                        : \"fill-white dark:fill-zinc-950\",\n                    )}\n                  >\n                    {priceFormatter.format(tagPrice)}\n                  </text>\n                </g>\n              )}\n            </svg>\n          )\n        )}\n      </div>\n\n      <p className=\"sr-only\" aria-live=\"polite\">\n        {announcement}\n      </p>\n    </div>\n  );\n});\n\nCandlestickChart.displayName = \"CandlestickChart\";\n\n",
      "type": "registry:ui",
      "target": "@ui/candlestick-chart.tsx"
    },
    {
      "path": "src/lib/utils.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n",
      "type": "registry:lib"
    }
  ]
}
