{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "candlestick-chart",
  "type": "registry:component",
  "title": "Candlestick Chart",
  "description": "A composable OHLC candlestick chart with gradients, patterns, tooltips, and hover interactions",
  "dependencies": [
    "@visx/scale@4.0.1-alpha.0",
    "@visx/responsive@4.0.1-alpha.0",
    "d3-array",
    "motion"
  ],
  "registryDependencies": [
    "@bklit/chart-context",
    "@bklit/chart-animation",
    "@bklit/grid",
    "@bklit/x-axis",
    "@bklit/y-axis",
    "@bklit/chart-tooltip",
    "@bklit/utils"
  ],
  "files": [
    {
      "path": "src/charts/candlestick-chart.tsx",
      "content": "\"use client\";\n\nimport { ParentSize } from \"@visx/responsive\";\nimport { scaleLinear, scaleTime } from \"@visx/scale\";\nimport { bisector } from \"d3-array\";\nimport type { Transition } from \"motion/react\";\nimport {\n  Children,\n  isValidElement,\n  memo,\n  type ReactElement,\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  isClipExcludedComponent,\n  isPostOverlayComponent,\n  isUnderlayComponent,\n} from \"./chart-child-passthrough\";\nimport { ChartProvider, type LineConfig, type Margin } from \"./chart-context\";\nimport { shortDateFmt } from \"./chart-formatters\";\nimport { DEFAULT_CHART_LIFECYCLE } from \"./chart-phase\";\nimport {\n  decimateOhlcData,\n  maxRenderPointsForWidth,\n} from \"./decimate-time-series\";\nimport { extractReferenceAreaConfigs } from \"./reference-area-config\";\nimport { useChartInteraction } from \"./use-chart-interaction\";\nimport { wrapSingleYScale } from \"./y-axis-scales\";\n\nexport interface OHLCDataPoint {\n  date: Date;\n  open: number;\n  high: number;\n  low: number;\n  close: number;\n}\n\nexport interface CandlestickChartProps {\n  /** OHLC data array */\n  data: OHLCDataPoint[];\n  /** Key in data for the x-axis (date). Default: \"date\" */\n  xDataKey?: string;\n  /** Chart margins */\n  margin?: Partial<Margin>;\n  /** Animation duration in milliseconds. Default: 1500 */\n  animationDuration?: number;\n  /** Motion enter transition (spring or cubic-bezier tween). */\n  enterTransition?: Transition;\n  /** Signature of motion URL state — triggers enter replay when it changes. */\n  revealSignature?: string;\n  /** Aspect ratio as \"width / height\". Default: \"2 / 1\" */\n  aspectRatio?: string;\n  /** Additional class name for the container */\n  className?: string;\n  /** Inline styles for the container (e.g. { height: 320 }) */\n  style?: React.CSSProperties;\n  /** Gap between candles as fraction of slot width (0–1). Default: 0.2. Ignored when candleWidth is set. */\n  candleGap?: number;\n  /** Fixed candle body width in pixels. If set, overrides candleGap. */\n  candleWidth?: number;\n  /** When set, xScale uses this domain instead of deriving from data. Use with brush so main chart and strip share the same scale. */\n  xDomain?: [Date, Date];\n  /** When xDomain is set, use this as the number of slots for scale padding (e.g. full data length). */\n  xDomainSlotCount?: number;\n  /** Child components (Candlestick, Grid, XAxis, YAxis, ChartTooltip, etc.) */\n  children: ReactNode;\n}\n\nconst DEFAULT_MARGIN: Margin = { top: 40, right: 40, bottom: 40, left: 40 };\n\ninterface ChartInnerProps {\n  width: number;\n  height: number;\n  data: Record<string, unknown>[];\n  xDataKey: string;\n  margin: Margin;\n  animationDuration: number;\n  enterTransition?: Transition;\n  revealSignature?: string;\n  candleGap: number;\n  candleWidthProp?: number;\n  xDomain?: [Date, Date];\n  xDomainSlotCount?: number;\n  children: ReactNode;\n  containerRef: React.RefObject<HTMLDivElement | null>;\n}\n\nfunction ChartInner(props: ChartInnerProps) {\n  const { width, height } = props;\n  if (width < 10 || height < 10) {\n    return null;\n  }\n  return <ChartCore {...props} />;\n}\n\nconst ChartCore = memo(function ChartCore({\n  width,\n  height,\n  data,\n  xDataKey,\n  margin,\n  animationDuration,\n  enterTransition,\n  revealSignature = \"\",\n  candleGap,\n  candleWidthProp,\n  xDomain,\n  xDomainSlotCount,\n  children,\n  containerRef,\n}: ChartInnerProps) {\n  const [isLoaded, setIsLoaded] = useState(false);\n  const [revealEpoch, setRevealEpoch] = useState(0);\n\n  const innerWidth = width - margin.left - margin.right;\n  const innerHeight = height - margin.top - margin.bottom;\n\n  const xAccessor = useCallback(\n    (d: Record<string, unknown>): Date => {\n      const value = d[xDataKey];\n      return value instanceof Date ? value : new Date(value as string | number);\n    },\n    [xDataKey]\n  );\n\n  const bisectDate = useMemo(\n    () => bisector<Record<string, unknown>, Date>((d) => xAccessor(d)).left,\n    [xAccessor]\n  );\n\n  const slotCount =\n    xDomain && xDomainSlotCount != null ? xDomainSlotCount : data.length;\n  const slotWidth = innerWidth / Math.max(slotCount, 1);\n  const xScale = useMemo(() => {\n    const minTime = xDomain\n      ? xDomain[0].getTime()\n      : Math.min(...data.map((d) => xAccessor(d).getTime()));\n    const maxTime = xDomain\n      ? xDomain[1].getTime()\n      : Math.max(...data.map((d) => xAccessor(d).getTime()));\n    const padding = slotWidth / 2;\n    return scaleTime({\n      range: [padding, innerWidth - padding],\n      domain: [minTime, maxTime],\n    });\n  }, [innerWidth, data, xAccessor, slotWidth, xDomain]);\n\n  const yScale = useMemo(() => {\n    let minVal = Number.POSITIVE_INFINITY;\n    let maxVal = Number.NEGATIVE_INFINITY;\n    for (const d of data) {\n      const low = d.low as number | undefined;\n      const high = d.high as number | undefined;\n      if (typeof low === \"number\" && low < minVal) {\n        minVal = low;\n      }\n      if (typeof high === \"number\" && high > maxVal) {\n        maxVal = high;\n      }\n    }\n    if (minVal === Number.POSITIVE_INFINITY) {\n      minVal = 0;\n    }\n    if (maxVal === Number.NEGATIVE_INFINITY) {\n      maxVal = 100;\n    }\n    const padding = (maxVal - minVal) * 0.05 || 1;\n    return scaleLinear({\n      range: [innerHeight, 0],\n      domain: [minVal - padding, maxVal + padding],\n      nice: true,\n    });\n  }, [innerHeight, data]);\n\n  const columnWidth = slotWidth;\n  const bandWidth = candleWidthProp ?? slotWidth * (1 - candleGap);\n\n  const lines: LineConfig[] = useMemo(\n    () => [\n      { dataKey: \"close\", stroke: \"var(--chart-line-primary)\", strokeWidth: 0 },\n    ],\n    []\n  );\n\n  const renderData = useMemo(\n    () => decimateOhlcData(data, maxRenderPointsForWidth(innerWidth)),\n    [data, innerWidth]\n  );\n\n  const dateLabels = useMemo(\n    () => data.map((d) => shortDateFmt.format(xAccessor(d))),\n    [data, xAccessor]\n  );\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: revealSignature\n  useEffect(() => {\n    setRevealEpoch((n) => n + 1);\n    setIsLoaded(false);\n    const timer = setTimeout(() => setIsLoaded(true), animationDuration);\n    return () => clearTimeout(timer);\n  }, [animationDuration, revealSignature]);\n\n  const {\n    tooltipData,\n    setTooltipData,\n    selection,\n    clearSelection,\n    interactionHandlers,\n    interactionStyle,\n  } = useChartInteraction({\n    xScale,\n    yScale,\n    yScales: wrapSingleYScale(yScale),\n    data,\n    lines,\n    margin,\n    xAccessor,\n    bisectDate,\n    canInteract: isLoaded,\n  });\n\n  const hoveredCandleIndex = tooltipData?.index ?? null;\n\n  const isDefsComponent = (child: ReactElement): boolean => {\n    const displayName =\n      (child.type as { displayName?: string })?.displayName ||\n      (child.type as { name?: string })?.name ||\n      \"\";\n    return (\n      displayName.includes(\"Gradient\") ||\n      displayName.includes(\"Pattern\") ||\n      displayName === \"LinearGradient\" ||\n      displayName === \"RadialGradient\" ||\n      displayName === \"Lines\" ||\n      displayName === \"PatternLines\"\n    );\n  };\n\n  const defsChildren: ReactElement[] = [];\n  const clipExcludedChildren: ReactElement[] = [];\n  const underlayChildren: ReactElement[] = [];\n  const preOverlayChildren: ReactElement[] = [];\n  const postOverlayChildren: ReactElement[] = [];\n  Children.forEach(children, (child) => {\n    if (!isValidElement(child)) {\n      return;\n    }\n    if (isDefsComponent(child)) {\n      defsChildren.push(child);\n    } else if (isPostOverlayComponent(child)) {\n      postOverlayChildren.push(child);\n    } else if (isClipExcludedComponent(child)) {\n      clipExcludedChildren.push(child);\n    } else if (isUnderlayComponent(child)) {\n      underlayChildren.push(child);\n    } else {\n      preOverlayChildren.push(child);\n    }\n  });\n\n  const referenceAreas = useMemo(\n    () => extractReferenceAreaConfigs(children),\n    [children]\n  );\n\n  const yScales = useMemo(() => wrapSingleYScale(yScale), [yScale]);\n\n  const contextValue = useMemo(\n    () => ({\n      ...DEFAULT_CHART_LIFECYCLE,\n      data,\n      renderData,\n      xScale,\n      yScale,\n      yScales,\n      width,\n      height,\n      innerWidth,\n      innerHeight,\n      margin,\n      columnWidth,\n      tooltipData,\n      setTooltipData,\n      containerRef,\n      lines,\n      referenceAreas,\n      isLoaded,\n      animationDuration,\n      enterTransition,\n      revealEpoch,\n      xAccessor,\n      dateLabels,\n      selection: selection ?? null,\n      clearSelection,\n      bandWidth,\n      hoveredCandleIndex,\n    }),\n    [\n      data,\n      renderData,\n      xScale,\n      yScale,\n      yScales,\n      width,\n      height,\n      innerWidth,\n      innerHeight,\n      margin,\n      columnWidth,\n      tooltipData,\n      setTooltipData,\n      containerRef,\n      lines,\n      referenceAreas,\n      isLoaded,\n      animationDuration,\n      enterTransition,\n      revealEpoch,\n      xAccessor,\n      dateLabels,\n      selection,\n      clearSelection,\n      bandWidth,\n      hoveredCandleIndex,\n    ]\n  );\n\n  return (\n    <ChartProvider value={contextValue}>\n      <svg aria-hidden=\"true\" height={height} width={width}>\n        <defs>\n          {/* Default vertical gradients for positive/negative candles (emerald / red) */}\n          <linearGradient id=\"candlestick-positive\" x1=\"0\" x2=\"0\" y1=\"1\" y2=\"0\">\n            <stop offset=\"0%\" stopColor=\"var(--color-emerald-500)\" />\n            <stop offset=\"100%\" stopColor=\"var(--color-emerald-500)\" />\n          </linearGradient>\n          <linearGradient id=\"candlestick-negative\" x1=\"0\" x2=\"0\" y1=\"1\" y2=\"0\">\n            <stop offset=\"0%\" stopColor=\"var(--color-red-500)\" />\n            <stop offset=\"100%\" stopColor=\"var(--color-red-500)\" />\n          </linearGradient>\n          {defsChildren}\n        </defs>\n        <rect fill=\"transparent\" height={height} width={width} x={0} y={0} />\n        <g\n          {...interactionHandlers}\n          style={interactionStyle}\n          transform={`translate(${margin.left},${margin.top})`}\n        >\n          <rect\n            fill=\"transparent\"\n            height={innerHeight}\n            width={innerWidth}\n            x={0}\n            y={0}\n          />\n          {clipExcludedChildren}\n          {underlayChildren}\n          {preOverlayChildren}\n          {postOverlayChildren}\n        </g>\n      </svg>\n    </ChartProvider>\n  );\n});\n\nexport function CandlestickChart({\n  data,\n  xDataKey = \"date\",\n  margin: marginProp,\n  animationDuration = 1100,\n  enterTransition,\n  revealSignature,\n  aspectRatio = \"2 / 1\",\n  className = \"\",\n  style,\n  candleGap = 0.2,\n  candleWidth,\n  xDomain,\n  xDomainSlotCount,\n  children,\n}: CandlestickChartProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const margin = { ...DEFAULT_MARGIN, ...marginProp };\n  const dataAsRecords = data as unknown as Record<string, unknown>[];\n\n  return (\n    <div\n      className={cn(\"relative w-full\", className)}\n      ref={containerRef}\n      style={{ aspectRatio, touchAction: \"none\", ...style }}\n    >\n      <ParentSize debounceTime={10}>\n        {({ width, height }) => (\n          <ChartInner\n            animationDuration={animationDuration}\n            candleGap={candleGap}\n            candleWidthProp={candleWidth}\n            containerRef={containerRef}\n            data={dataAsRecords}\n            enterTransition={enterTransition}\n            height={height}\n            margin={margin}\n            revealSignature={revealSignature}\n            width={width}\n            xDataKey={xDataKey}\n            xDomain={xDomain}\n            xDomainSlotCount={xDomainSlotCount}\n          >\n            {children}\n          </ChartInner>\n        )}\n      </ParentSize>\n    </div>\n  );\n}\n\nCandlestickChart.displayName = \"CandlestickChart\";\n\nexport default CandlestickChart;\n",
      "type": "registry:component",
      "target": "components/charts/candlestick-chart.tsx"
    },
    {
      "path": "src/charts/chart-child-passthrough.ts",
      "content": "import {\n  Children,\n  cloneElement,\n  Fragment,\n  isValidElement,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\n\n/** Marker on wrapper components whose single child should inherit clip classification. */\nexport const CHART_CLIP_PASSTHROUGH = \"__chartClipPassthrough\" as const;\n\nexport function isChartClipPassthrough(type: unknown): boolean {\n  return (\n    typeof type === \"function\" &&\n    (type as { [CHART_CLIP_PASSTHROUGH]?: boolean })[CHART_CLIP_PASSTHROUGH] ===\n      true\n  );\n}\n\n/** Unwrap visibility wrappers so `Grid` / axes stay outside the series clip. */\nexport function resolveChartChildElement(child: ReactElement): ReactElement {\n  if (isChartClipPassthrough(child.type)) {\n    const inner = (child.props as { children?: unknown }).children;\n    if (isValidElement(inner)) {\n      return resolveChartChildElement(inner);\n    }\n  }\n  return child;\n}\n\n/** Walk chart children, flattening React fragments (studio often groups layers in `<>...</>`). */\nexport function forEachChartChild(\n  children: ReactNode,\n  callback: (child: ReactElement, index: number) => void\n) {\n  let index = 0;\n  const visit = (nodes: ReactNode) => {\n    Children.forEach(nodes, (child) => {\n      if (!isValidElement(child)) {\n        return;\n      }\n      if (child.type === Fragment) {\n        visit((child.props as { children?: ReactNode }).children);\n        return;\n      }\n      callback(child, index);\n      index += 1;\n    });\n  };\n  visit(children);\n}\n\nconst CLIP_EXCLUDED_COMPONENT_NAMES = new Set([\n  \"Background\",\n  \"Grid\",\n  \"XAxis\",\n  \"YAxis\",\n  \"BarXAxis\",\n  \"BarYAxis\",\n  \"LiveXAxis\",\n  \"LiveYAxis\",\n]);\n\nconst UNDERLAY_COMPONENT_NAMES = new Set([\"ReferenceArea\", \"BarColumnTrack\"]);\n\n/** Markers render after the interaction overlay so they stay clickable. */\nexport function isPostOverlayComponent(child: ReactElement): boolean {\n  const childType = child.type as {\n    displayName?: string;\n    name?: string;\n    __isChartMarkers?: boolean;\n    __isPostOverlay?: boolean;\n  };\n\n  if (childType.__isChartMarkers || childType.__isPostOverlay) {\n    return true;\n  }\n\n  const componentName =\n    typeof child.type === \"function\"\n      ? childType.displayName || childType.name || \"\"\n      : \"\";\n\n  return (\n    componentName === \"ChartMarkers\" ||\n    componentName === \"MarkerGroup\" ||\n    componentName === \"ChartBrush\"\n  );\n}\n\n/** Renders above grid/axes but below series; excluded from grow-clip reveal. */\nexport function isUnderlayComponent(child: ReactElement): boolean {\n  const childType = child.type as { displayName?: string; name?: string };\n  const componentName =\n    typeof child.type === \"function\"\n      ? childType.displayName || childType.name || \"\"\n      : \"\";\n  return UNDERLAY_COMPONENT_NAMES.has(componentName);\n}\n\n/** Grid and axes stay visible during series clip reveal (e.g. loading → ready). */\nexport function isClipExcludedComponent(child: ReactElement): boolean {\n  const childType = child.type as { displayName?: string; name?: string };\n  const componentName =\n    typeof child.type === \"function\"\n      ? childType.displayName || childType.name || \"\"\n      : \"\";\n  return CLIP_EXCLUDED_COMPONENT_NAMES.has(componentName);\n}\n\n/** SVG layer lists from chart shells need stable keys when rendered as arrays. */\nexport function renderKeyedChartLayers(children: ReactElement[]) {\n  return children.map((child, index) =>\n    cloneElement(child, { key: child.key ?? `chart-layer-${index}` })\n  );\n}\n",
      "type": "registry:lib",
      "target": "components/charts/chart-child-passthrough.ts"
    },
    {
      "path": "src/charts/chart-legend-hover.tsx",
      "content": "\"use client\";\n\nimport { createContext, type ReactNode, useContext, useMemo } from \"react\";\n\ninterface ChartLegendHoverContextValue {\n  hoveredIndex: number | null;\n  setHoveredIndex: (index: number | null) => void;\n}\n\nconst ChartLegendHoverContext =\n  createContext<ChartLegendHoverContextValue | null>(null);\n\nexport function ChartLegendHoverProvider({\n  hoveredIndex,\n  onHoverChange,\n  children,\n}: {\n  hoveredIndex: number | null;\n  onHoverChange: (index: number | null) => void;\n  children: ReactNode;\n}) {\n  const value = useMemo(\n    () => ({ hoveredIndex, setHoveredIndex: onHoverChange }),\n    [hoveredIndex, onHoverChange]\n  );\n\n  return (\n    <ChartLegendHoverContext.Provider value={value}>\n      {children}\n    </ChartLegendHoverContext.Provider>\n  );\n}\n\nexport function useChartLegendHover(): ChartLegendHoverContextValue {\n  const context = useContext(ChartLegendHoverContext);\n  return (\n    context ?? {\n      hoveredIndex: null,\n      setHoveredIndex: () => {\n        /* noop outside ChartLegendHoverProvider */\n      },\n    }\n  );\n}\n",
      "type": "registry:component",
      "target": "components/charts/chart-legend-hover.tsx"
    },
    {
      "path": "src/charts/candlestick.tsx",
      "content": "\"use client\";\n\nimport type { Transition } from \"motion/react\";\nimport { motion } from \"motion/react\";\nimport { memo, useMemo } from \"react\";\nimport { useChart } from \"./chart-context\";\nimport { useChartLegendHover } from \"./chart-legend-hover\";\nimport { transitionWithDelay } from \"./motion-utils\";\n\nconst DEFAULT_POSITIVE = \"url(#candlestick-positive)\";\nconst DEFAULT_NEGATIVE = \"url(#candlestick-negative)\";\n\nconst SOLID_POSITIVE = \"var(--color-emerald-500)\";\nconst SOLID_NEGATIVE = \"var(--color-red-500)\";\nconst WICK_WIDTH = 1.5;\n\nexport interface CandlestickProps {\n  /** Whether to animate the candlesticks. Default: true */\n  animate?: boolean;\n  /** Fill for positive (close >= open) candles. Color or url(#gradient). Default: emerald */\n  positiveFill?: string;\n  /** Fill for negative candles. Color or url(#gradient). Default: red */\n  negativeFill?: string;\n  /** Optional pattern URL for body only (e.g. url(#pattern)). When set, body is drawn solid first, then pattern overlaid and masked to the body rect. */\n  bodyPatternPositive?: string;\n  /** Optional pattern URL for negative candle body. */\n  bodyPatternNegative?: string;\n  /** Inner border width on the body (drawn inside so it does not expand the shape). Default: 0 (off). */\n  insideStrokeWidth?: number;\n  /** Opacity when another candle is hovered. Default: 0.3 */\n  fadedOpacity?: number;\n  /** Dim non-hovered candles on hover. Default: true */\n  showHoverFade?: boolean;\n}\n\ninterface CandleGeometry {\n  time: number;\n  centerX: number;\n  bodyTop: number;\n  bodyHeight: number;\n  bodyLeft: number;\n  candleWidth: number;\n  wickTop: number;\n  wickHeight: number;\n  wickLeft: number;\n  bodySolidFill: string;\n  wickFill: string;\n  bodyPattern?: string;\n  insideStrokeWidth: number;\n  isPositive: boolean;\n}\n\nfunction getSolidColor(isPositive: boolean): string {\n  return isPositive ? SOLID_POSITIVE : SOLID_NEGATIVE;\n}\n\nfunction computeGeometries(\n  renderData: Record<string, unknown>[],\n  xScale: (value: Date) => number | undefined,\n  yScale: (value: number) => number | undefined,\n  xAccessor: (d: Record<string, unknown>) => Date,\n  candleWidth: number,\n  positiveFill: string,\n  negativeFill: string,\n  bodyPatternPositive: string | undefined,\n  bodyPatternNegative: string | undefined,\n  insideStrokeWidth: number\n): CandleGeometry[] {\n  return renderData.map((d) => {\n    const date = xAccessor(d);\n    const open = d.open as number;\n    const high = d.high as number;\n    const low = d.low as number;\n    const close = d.close as number;\n    const centerX = xScale(date) ?? 0;\n    const yHigh = yScale(high) ?? 0;\n    const yLow = yScale(low) ?? 0;\n    const yOpen = yScale(open) ?? 0;\n    const yClose = yScale(close) ?? 0;\n    const bodyTop = Math.min(yOpen, yClose);\n    const bodyHeight = Math.abs(yClose - yOpen) || 1;\n    const bodyLeft = centerX - candleWidth / 2;\n    const wickTop = Math.min(yHigh, yLow);\n    const wickHeight = Math.abs(yLow - yHigh) || 1;\n    const isPositive = close >= open;\n    const fill = isPositive ? positiveFill : negativeFill;\n    const bodyPattern = isPositive ? bodyPatternPositive : bodyPatternNegative;\n    const hasPatternOverlay = Boolean(bodyPattern);\n    const bodySolidFill = hasPatternOverlay ? getSolidColor(isPositive) : fill;\n\n    return {\n      time: date.getTime(),\n      centerX,\n      bodyTop,\n      bodyHeight,\n      bodyLeft,\n      candleWidth,\n      wickTop,\n      wickHeight,\n      wickLeft: centerX - WICK_WIDTH / 2,\n      bodySolidFill,\n      wickFill: hasPatternOverlay ? bodySolidFill : fill,\n      bodyPattern: hasPatternOverlay ? bodyPattern : undefined,\n      insideStrokeWidth,\n      isPositive,\n    };\n  });\n}\n\nfunction geometryDimOpacity(\n  geometry: CandleGeometry,\n  fadedOpacity: number,\n  legendHoveredIndex: number | null,\n  hoveredTime: number | null\n): number {\n  if (legendHoveredIndex !== null) {\n    const dimFromLegend =\n      (legendHoveredIndex === 0 && !geometry.isPositive) ||\n      (legendHoveredIndex === 1 && geometry.isPositive);\n    return dimFromLegend ? fadedOpacity : 1;\n  }\n  if (hoveredTime !== null && geometry.time !== hoveredTime) {\n    return fadedOpacity;\n  }\n  return 1;\n}\n\nconst CandlestickBody = memo(function CandlestickBody({\n  geometry,\n}: {\n  geometry: CandleGeometry;\n}) {\n  const {\n    wickLeft,\n    wickTop,\n    wickHeight,\n    wickFill,\n    bodyLeft,\n    bodyTop,\n    bodyHeight,\n    candleWidth,\n    bodySolidFill,\n    bodyPattern,\n    insideStrokeWidth,\n  } = geometry;\n\n  return (\n    <>\n      <rect\n        fill={wickFill}\n        height={wickHeight}\n        width={WICK_WIDTH}\n        x={wickLeft}\n        y={wickTop}\n      />\n      <rect\n        fill={bodySolidFill}\n        height={bodyHeight}\n        rx={1}\n        ry={1}\n        stroke={bodySolidFill}\n        strokeWidth={1}\n        width={candleWidth}\n        x={bodyLeft}\n        y={bodyTop}\n      />\n      {bodyPattern ? (\n        <rect\n          fill={bodyPattern}\n          height={bodyHeight}\n          rx={1}\n          ry={1}\n          width={candleWidth}\n          x={bodyLeft}\n          y={bodyTop}\n        />\n      ) : null}\n      {insideStrokeWidth > 0 ? (\n        <rect\n          fill=\"none\"\n          height={bodyHeight - insideStrokeWidth}\n          rx={1}\n          ry={1}\n          stroke={bodySolidFill}\n          strokeWidth={insideStrokeWidth}\n          width={candleWidth - insideStrokeWidth}\n          x={bodyLeft + insideStrokeWidth / 2}\n          y={bodyTop + insideStrokeWidth / 2}\n        />\n      ) : null}\n    </>\n  );\n});\n\nconst CandlestickBodies = memo(function CandlestickBodies({\n  geometries,\n  fadedOpacity,\n  legendHoveredIndex,\n  hoveredTime,\n}: {\n  geometries: CandleGeometry[];\n  fadedOpacity: number;\n  legendHoveredIndex: number | null;\n  hoveredTime: number | null;\n}) {\n  return (\n    <>\n      {geometries.map((geometry) => (\n        <g\n          key={geometry.time}\n          opacity={geometryDimOpacity(\n            geometry,\n            fadedOpacity,\n            legendHoveredIndex,\n            hoveredTime\n          )}\n          style={{ transition: \"opacity 0.15s ease-in-out\" }}\n        >\n          <CandlestickBody geometry={geometry} />\n        </g>\n      ))}\n    </>\n  );\n});\n\ninterface AnimatedCandleProps {\n  geometry: CandleGeometry;\n  delay: number;\n  enterTransition: Transition;\n  revealEpoch: number;\n}\n\nfunction AnimatedCandle({\n  geometry,\n  delay,\n  enterTransition,\n  revealEpoch,\n}: AnimatedCandleProps) {\n  const t = transitionWithDelay(enterTransition, delay);\n  const bodyOrigin = `${geometry.centerX}px ${geometry.bodyTop + geometry.bodyHeight / 2}px`;\n  const wickCenterY = geometry.wickTop + geometry.wickHeight / 2;\n\n  return (\n    <motion.g\n      animate={{ opacity: 1 }}\n      initial={{ opacity: 0 }}\n      key={`candle-enter-${geometry.time}-${revealEpoch}`}\n      style={{ transformOrigin: `${geometry.centerX}px ${wickCenterY}px` }}\n      transition={{ ...t, opacity: { duration: 0.15 } }}\n    >\n      <motion.rect\n        animate={{ scaleY: 1 }}\n        fill={geometry.wickFill}\n        height={geometry.wickHeight}\n        initial={{ scaleY: 0 }}\n        style={{ transformOrigin: `${geometry.centerX}px ${wickCenterY}px` }}\n        transition={t}\n        width={WICK_WIDTH}\n        x={geometry.wickLeft}\n        y={geometry.wickTop}\n      />\n      <motion.rect\n        animate={{ scaleY: 1 }}\n        fill={geometry.bodySolidFill}\n        height={geometry.bodyHeight}\n        initial={{ scaleY: 0 }}\n        rx={1}\n        ry={1}\n        stroke={geometry.bodySolidFill}\n        strokeWidth={1}\n        style={{ transformOrigin: bodyOrigin }}\n        transition={t}\n        width={geometry.candleWidth}\n        x={geometry.bodyLeft}\n        y={geometry.bodyTop}\n      />\n      {geometry.bodyPattern ? (\n        <motion.rect\n          animate={{ scaleY: 1 }}\n          fill={geometry.bodyPattern}\n          height={geometry.bodyHeight}\n          initial={{ scaleY: 0 }}\n          rx={1}\n          ry={1}\n          style={{ transformOrigin: bodyOrigin }}\n          transition={t}\n          width={geometry.candleWidth}\n          x={geometry.bodyLeft}\n          y={geometry.bodyTop}\n        />\n      ) : null}\n    </motion.g>\n  );\n}\n\nexport function Candlestick({\n  animate = true,\n  positiveFill = DEFAULT_POSITIVE,\n  negativeFill = DEFAULT_NEGATIVE,\n  bodyPatternPositive,\n  bodyPatternNegative,\n  insideStrokeWidth = 0,\n  fadedOpacity = 0.3,\n  showHoverFade = true,\n}: CandlestickProps) {\n  const {\n    data,\n    xScale,\n    yScale,\n    xAccessor,\n    animationDuration,\n    enterTransition,\n    revealEpoch = 0,\n    isLoaded,\n    bandWidth,\n    columnWidth,\n    hoveredCandleIndex,\n  } = useChart();\n  const { hoveredIndex: legendHoveredIndex } = useChartLegendHover();\n\n  const candleWidth = Math.min(bandWidth ?? columnWidth * 0.8, columnWidth);\n\n  const geometries = useMemo(\n    () =>\n      computeGeometries(\n        data,\n        xScale,\n        yScale,\n        xAccessor,\n        candleWidth,\n        positiveFill,\n        negativeFill,\n        bodyPatternPositive,\n        bodyPatternNegative,\n        insideStrokeWidth\n      ),\n    [\n      data,\n      xScale,\n      yScale,\n      xAccessor,\n      candleWidth,\n      positiveFill,\n      negativeFill,\n      bodyPatternPositive,\n      bodyPatternNegative,\n      insideStrokeWidth,\n    ]\n  );\n\n  const hoveredTime = useMemo(() => {\n    if (hoveredCandleIndex == null) {\n      return null;\n    }\n    const point = data[hoveredCandleIndex];\n    return point ? xAccessor(point).getTime() : null;\n  }, [hoveredCandleIndex, data, xAccessor]);\n\n  const highlightGeometry = useMemo(() => {\n    if (hoveredCandleIndex == null) {\n      return null;\n    }\n    const point = data[hoveredCandleIndex];\n    if (!point) {\n      return null;\n    }\n    return (\n      computeGeometries(\n        [point],\n        xScale,\n        yScale,\n        xAccessor,\n        candleWidth,\n        positiveFill,\n        negativeFill,\n        bodyPatternPositive,\n        bodyPatternNegative,\n        insideStrokeWidth\n      )[0] ?? null\n    );\n  }, [\n    hoveredCandleIndex,\n    data,\n    xScale,\n    yScale,\n    xAccessor,\n    candleWidth,\n    positiveFill,\n    negativeFill,\n    bodyPatternPositive,\n    bodyPatternNegative,\n    insideStrokeWidth,\n  ]);\n\n  const defaultEnter: Transition = {\n    type: \"spring\",\n    duration: 0.8,\n    bounce: 0.15,\n  };\n  const enter = enterTransition ?? defaultEnter;\n  const staggerDelayMs =\n    data.length > 0 ? (animationDuration * 0.6) / data.length : 0;\n\n  if (animate && !isLoaded) {\n    return (\n      <g className=\"chart-candlesticks\">\n        {geometries.map((geometry, index) => (\n          <AnimatedCandle\n            delay={(index * staggerDelayMs) / 1000}\n            enterTransition={enter}\n            geometry={geometry}\n            key={geometry.time}\n            revealEpoch={revealEpoch}\n          />\n        ))}\n      </g>\n    );\n  }\n\n  return (\n    <g className=\"chart-candlesticks\">\n      <CandlestickBodies\n        fadedOpacity={fadedOpacity}\n        geometries={geometries}\n        hoveredTime={showHoverFade ? hoveredTime : null}\n        legendHoveredIndex={legendHoveredIndex}\n      />\n      {highlightGeometry ? (\n        <g>\n          <CandlestickBody geometry={highlightGeometry} />\n        </g>\n      ) : null}\n    </g>\n  );\n}\n\nCandlestick.displayName = \"Candlestick\";\n\nexport default Candlestick;\n",
      "type": "registry:component",
      "target": "components/charts/candlestick.tsx"
    }
  ]
}