{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chart",
  "title": "Chart",
  "description": "선언형 차트 컴포넌트(ChartContainer·BarSeries·LineSeries·AreaSeries·Grid·Axis·Tooltip·Legend). recharts를 제거하고 스케일·틱 계산을 내장하며 형상은 웹 SVG·네이티브 react-native-svg(선택 peer 의존 — react-native-svg)로 그린다. 색은 fill-chart-1..5 토큰 클래스(소비자 앱이 --chart-1..5 정의).",
  "dependencies": [
    "@unikit-bridge/ui"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/chart.tsx",
      "content": "// Chart — shadcn/ui（new-york-v4） chart.tsx 유니버설 포팅 （사이클 40, T-3a）.\n// 원본: shadcn-ref/ui/chart.tsx — recharts 2.x 기반. ChartContainer가\n// ResponsiveContainer으로 recharts 트리를 감쌌고 ChartTooltip/ChartLegend는\n// recharts Primitive를 그대로 재수출했다.\n//\n// 포팅 판단（원본 대비 차이이유）:\n// - recharts 제거. ChartContainer（config, children） 시그니처는 유지하되 children은\n//   recharts 차트가 아니라 이 파일의 선언형 요소 조합으로 바뀌었다 — 시리즈\n//   （ChartBarSeriesChartLineSeriesChartAreaSeries）, 보조（ChartGridChartXAxis\n//   ChartYAxis）, 오버레이（ChartTooltip）, 범례（ChartLegend）. 스케일틱 계산을\n//   내장하고 형상은 @unikit-bridge/ui/chart 렌더러（ChartSurfaceChartPath\n//   ChartRectChartLineChartTextChartCircle）가 그린다（골격 계약 그대로）.\n// - 색상 전략（확정）: 시리즈 기본색은 getChartColor（index, kind）가 리터럴 토큰\n//   클래스（fill-chart-1..5 / stroke-chart-1..5 / bg-chart-1..5, 5색 순환）를\n//   반환하고 이를 className으로 싣는다. 웹은 소비자 앱 Tailwind v4가 소비자\n//   globals.css의 --chart-N 변수（shadcn 차트 표준 토큰 — 값의 소유자는 소비자\n//   앱）에서 유틸리티를 생성하고, 네이티브는 uniwind 테마에 --chart-N이 등록돼\n//   있으면 적용된다（없으면 폐기 — 렌더러 골격의 \"className 원칙, prop 폴백\"\n//   계약과 동일）. config［key］.color가 명시 문자열이면 그것이 우선해 fill/stroke\n//   prop에 직접 전달된다（#hexhsl（）var（--chart-2） 모두 웹은 확실히 동작,\n//   네이티브는 리터럴 색 확실var（）은 uniwind 의존）. config.theme{light,dark}는\n//   스킴 조회 계약이 없어 light 값을 쓴다（다크 전환 미지원 — 로드맵）.\n// - ChartStyle은 원본 시그니처를 유지하되 no-op: CSS 변수 <style> 주입은\n//   네이티브에서 불가능하고（RN은 <style>을 렌더할 수 없음） 위 전략은 JS 레벨\n//   이라 --color-* 주입이 불필요하다.\n// - 접근성: 원본 accessibilityLayer（recharts 옵션） 대신 ChartSurface에 시리즈\n//   요약 accessibilityLabel（각 시리즈 라벨최소~최대항목 수）을 기본 제공하고\n//   컨테이너 accessibilityLabel prop으로 덮어쓴다（설계 확정 사양）.\n// - 툴팁 상호작용: 웹은 ChartSurface hover（렌더러 onPointMove）, 네이티브웹\n//   공통으로 SVG 위 투명 오버레이 Rect 탭（렌더러 onPress — 같은 지점 재탭 시\n//   닫기）. ChartTooltip은 SVG 밖 absolute 오버레이（pointerEvents 통과）로\n//   렌더된다. ChartTooltipContent는 원본 props（indicatorlabelFormatter\n//   formatternameKeylabelKey 등）를 유지하되 payloadactive은 recharts가\n//   아니라 context（활성 데이터 행 + 시리즈）에서 얻는다. formatter 시그니처는\n//   （value, name） 2인자로 단순화했다.\n// - 반응형 크기 미지원: 브리지에 onLayout 계약이 없어 ResponsiveContainer을\n//   대체하지 못한다. width/height props（기본 320×200 = 원본 INITIAL_DIMENSION）\n//   로 명시 크기를 받는다（원본 initialDimension prop의 대체 API）. 원본의\n//   data-slot/data-chart 속성recharts 자손 셀렉터 클래스（［&_.recharts-*］）는\n//   recharts 전용이라 삭제했다.\n// - 미지원（recharts 대비, 로드맵）: 스택극좌표방사형브러시줌참조선\n//   애니메이션다중 y축음수 값（y 도메인 0 고정）. x 라벨 겹침은 24px당 1라벨\n//   스킵으로만 방지한다. 레이아웃 상수（패딩 좌48우8상8하24, 틱 4구간）는\n//   고정 — 좌 패딩은 \"20,000\"급 6자 라벨이 plotLeft-8 앵커에서 캔버스 밖으로\n//   새지 않도록 40→48로 올렸다（2026-11-12 블록 검증）. 그보다 길면\n//   tickFormat으로 줄인다.\n// - 유동 폭: 원본 ResponsiveContainer 근사로 width=\"fluid\"를 지원한다 —\n//   컨테이너 자체 폭을 ResizeObserver로 측정해 캔버스 폭으로 쓴다（웹 전용.\n//   네이티브는 ResizeObserver 계약이 없어 기본 320 폴백 — 고정 폭 권장）.\n// - 원본 임의값 클래스 정규화: min-w-［8rem］→min-w-32, rounded-［2px］→rounded-xs,\n//   border-［1.5px］→border-2（스텝 근사）. shadow-xl은 웹 전용（web:）으로 내렸다.\n//   dark: 변형은 없다（토큰이 스킴 단일값）. 원본 툴팁/범례 인디케이터의\n//   ［&>svg］:* 자손 셀렉터（아이콘 크기）는 미지원 — icon 크기는 사용처가 지정한다.\nimport {\n  Children,\n  createContext,\n  forwardRef,\n  isValidElement,\n  useContext,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n  type ComponentType,\n  type ReactNode,\n  type RefObject,\n} from \"react\";\nimport {\n  Text,\n  View,\n  type BridgeViewRef,\n  type ViewProps,\n} from \"@unikit-bridge/ui/primitives\";\nimport {\n  ChartCircle,\n  ChartLine,\n  ChartPath,\n  ChartRect,\n  ChartSurface,\n  ChartText,\n} from \"@unikit-bridge/ui/chart\";\n\nimport { cn } from \"@/lib/utils\";\n\n// ---------------------------------------------------------------------------\n// 설정컨텍스트\n// ---------------------------------------------------------------------------\n\nexport type ChartConfig = Record<\n  string,\n  {\n    label?: ReactNode;\n    icon?: ComponentType;\n  } & (\n    | { color?: string; theme?: never }\n    | { color?: never; theme: Record<\"light\" | \"dark\", string> }\n  )\n>;\n\n/** 데이터 행 — x키（범주 문자열） + 시리즈 수치（문자열 라벨도 허용）. */\nexport type ChartDatum = Record<string, string | number | null | undefined>;\n\n/** 캔버스 내부 계산 결과 — 시리즈축격자툴팁이 공유한다. */\ntype ChartScale = {\n  plotLeft: number;\n  plotTop: number;\n  plotWidth: number;\n  plotHeight: number;\n  /** x 밴드（데이터 1행） 폭 */\n  bandWidth: number;\n  yMax: number;\n  xAt: (index: number) => number;\n  yAt: (value: number) => number;\n  ticksY: number[];\n};\n\ntype ChartContextValue = {\n  config: ChartConfig;\n  data: ChartDatum[];\n  xKey: string;\n  scale: ChartScale;\n  /** children의 시리즈 등장 순서（색 인덱스 기준） */\n  seriesKeys: string[];\n  activeIndex: number | null;\n  width: number;\n  height: number;\n};\n\nconst ChartContext = createContext<ChartContextValue | null>(null);\n\nfunction useChart(component: string): ChartContextValue {\n  const ctx = useContext(ChartContext);\n  if (!ctx) {\n    throw new Error(`<${component}>는 <ChartContainer> 안에서 사용해야 합니다.`);\n  }\n  return ctx;\n}\n\n// ---------------------------------------------------------------------------\n// 색상 — 시리즈 순번 토큰 클래스（원칙） + config 명시 색 prop（폴백 아닌 우선）\n// ---------------------------------------------------------------------------\n\nconst CHART_COLOR_CLASSES = {\n  fill: [\"fill-chart-1\", \"fill-chart-2\", \"fill-chart-3\", \"fill-chart-4\", \"fill-chart-5\"],\n  stroke: [\"stroke-chart-1\", \"stroke-chart-2\", \"stroke-chart-3\", \"stroke-chart-4\", \"stroke-chart-5\"],\n  bg: [\"bg-chart-1\", \"bg-chart-2\", \"bg-chart-3\", \"bg-chart-4\", \"bg-chart-5\"],\n} as const;\n\ntype ChartColorKind = keyof typeof CHART_COLOR_CLASSES;\n\n/**\n * 시리즈 순번 → chart-N 토큰 클래스. 리터럴 테이블에서 반환해야 Tailwind v4\n * 스캐너가 유틸리티를 생성한다（문자열 연결 금지）. 6번째 시리즈부터 5색 순환.\n */\nfunction getChartColor(index: number, kind: ChartColorKind = \"fill\"): string {\n  const classes = CHART_COLOR_CLASSES[kind];\n  return classes[Math.abs(index) % classes.length];\n}\n\ntype ResolvedSeriesColor = { className: string; color?: string };\n\n/** config 명시 색（colortheme.light）이 우선, 없으면 순번 토큰 클래스. */\nfunction resolveSeriesColor(\n  config: ChartConfig,\n  dataKey: string,\n  index: number,\n  kind: ChartColorKind,\n): ResolvedSeriesColor {\n  const entry = config[dataKey];\n  const explicit = entry?.color ?? entry?.theme?.light;\n  return explicit\n    ? { className: \"\", color: explicit }\n    : { className: getChartColor(index, kind) };\n}\n\n// ---------------------------------------------------------------------------\n// 스케일 계산\n// ---------------------------------------------------------------------------\n\nconst CHART_PADDING = { top: 8, right: 8, bottom: 24, left: 48 } as const;\nconst INITIAL_DIMENSION = { width: 320, height: 200 } as const;\n/** 툴팁 카드 추정 폭（min-w-32=128px） — 위치 클램프 계산용 동적 상수 */\nconst TOOLTIP_WIDTH = 128;\n\n/** 보기 좋은 틱 간격（1/2/5×10^k）으로 올림 — 틱 라벨이 언제나 정돈된 수가 된다. */\nfunction niceStep(value: number): number {\n  if (!Number.isFinite(value) || value <= 0) return 1;\n  const magnitude = 10 ** Math.floor(Math.log10(value));\n  const normalized = value / magnitude;\n  const step = normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10;\n  return step * magnitude;\n}\n\nfunction rawSeriesMax(data: ChartDatum[], seriesKeys: string[]): number {\n  let max = 0;\n  for (const datum of data) {\n    for (const key of seriesKeys) {\n      const value = datum[key];\n      if (typeof value === \"number\" && value > max) max = value;\n    }\n  }\n  return max;\n}\n\nfunction buildScale(\n  data: ChartDatum[],\n  seriesKeys: string[],\n  width: number,\n  height: number,\n): ChartScale {\n  const plotLeft = CHART_PADDING.left;\n  const plotTop = CHART_PADDING.top;\n  const plotWidth = Math.max(width - CHART_PADDING.left - CHART_PADDING.right, 1);\n  const plotHeight = Math.max(height - CHART_PADDING.top - CHART_PADDING.bottom, 1);\n  const count = data.length;\n  const bandWidth = count > 0 ? plotWidth / count : plotWidth;\n  // 틱 4구간 — 최대값/4를 niceStep으로 올려 라벨을 깔끔히 유지한다.\n  const step = niceStep(rawSeriesMax(data, seriesKeys) / 4);\n  const yMax = step * 4;\n  return {\n    plotLeft,\n    plotTop,\n    plotWidth,\n    plotHeight,\n    bandWidth,\n    yMax,\n    xAt: (index) => plotLeft + bandWidth * (index + 0.5),\n    // 음수 미지원 — 0으로 클램프（머리주석 참고）.\n    yAt: (value) => plotTop + plotHeight - (Math.max(value, 0) / yMax) * plotHeight,\n    ticksY: [0, step, step * 2, step * 3, step * 4],\n  };\n}\n\nfunction formatChartValue(value: number): string {\n  return value.toLocaleString();\n}\n\n/** 데이터 첫 행의 첫 문자열 값 키 — xKey 미지정 시 폴백. */\nfunction firstCategoryKey(data: ChartDatum[]): string {\n  const first = data[0];\n  if (first) {\n    for (const [key, value] of Object.entries(first)) {\n      if (typeof value === \"string\") return key;\n    }\n  }\n  return \"x\";\n}\n\n/** ChartSurface 시리즈 요약 라벨（원본 accessibilityLayer 대체）. */\nfunction summarizeSeries(\n  config: ChartConfig,\n  data: ChartDatum[],\n  xKey: string,\n  seriesKeys: string[],\n): string {\n  const xLabel = config[xKey]?.label;\n  const xName = typeof xLabel === \"string\" ? xLabel : xKey;\n  if (!seriesKeys.length) return `차트(${xName}) — 시리즈 없음`;\n  const parts = seriesKeys.map((key) => {\n    const label = config[key]?.label;\n    const name = typeof label === \"string\" ? label : key;\n    let min = Infinity;\n    let max = -Infinity;\n    for (const datum of data) {\n      const value = datum[key];\n      if (typeof value === \"number\") {\n        if (value < min) min = value;\n        if (value > max) max = value;\n      }\n    }\n    if (min === Infinity) return `${name}: 값 없음`;\n    return `${name} 최소 ${formatChartValue(min)} 최대 ${formatChartValue(max)}`;\n  });\n  return `차트(${xName}) — ${parts.join(\", \")} (${data.length}개 항목)`;\n}\n\n// ---------------------------------------------------------------------------\n// 시리즈 렌더러 — context의 스케일로 SVG 조각을 그린다\n// ---------------------------------------------------------------------------\n\ntype SeriesPoint = { index: number; value: number; x: number; y: number };\n\nfunction seriesPoints(ctx: ChartContextValue, dataKey: string): SeriesPoint[] {\n  const points: SeriesPoint[] = [];\n  ctx.data.forEach((datum, index) => {\n    const value = datum[dataKey];\n    if (typeof value === \"number\") {\n      points.push({ index, value, x: ctx.scale.xAt(index), y: ctx.scale.yAt(value) });\n    }\n  });\n  return points;\n}\n\nfunction seriesIndex(ctx: ChartContextValue, dataKey: string): number {\n  const index = ctx.seriesKeys.indexOf(dataKey);\n  return index >= 0 ? index : 0;\n}\n\nexport type ChartBarSeriesProps = {\n  dataKey: string;\n  className?: string;\n  /** 막대 모서리 반경（px） */\n  radius?: number;\n};\n\nfunction ChartBarSeries({ dataKey, className, radius = 4 }: ChartBarSeriesProps) {\n  const ctx = useChart(\"ChartBarSeries\");\n  const { scale } = ctx;\n  const color = resolveSeriesColor(ctx.config, dataKey, seriesIndex(ctx, dataKey), \"fill\");\n  // 인접 막대 사이 표면 간격 — 밴드의 60%만 채운다（최소 2px）.\n  const barWidth = Math.max(2, scale.bandWidth * 0.6);\n  const baseline = scale.plotTop + scale.plotHeight;\n  return (\n    <>\n      {ctx.data.map((datum, index) => {\n        const value = datum[dataKey];\n        if (typeof value !== \"number\") return null;\n        const y = scale.yAt(value);\n        return (\n          <ChartRect\n            key={index}\n            x={scale.xAt(index) - barWidth / 2}\n            y={y}\n            width={barWidth}\n            height={baseline - y}\n            rx={radius}\n            className={cn(color.className, className)}\n            fill={color.color}\n          />\n        );\n      })}\n    </>\n  );\n}\n\nfunction activePointOf(ctx: ChartContextValue, points: SeriesPoint[]): SeriesPoint | null {\n  if (ctx.activeIndex == null) return null;\n  return points.find((point) => point.index === ctx.activeIndex) ?? null;\n}\n\nexport type ChartLineSeriesProps = {\n  dataKey: string;\n  className?: string;\n  strokeWidth?: number;\n};\n\nfunction ChartLineSeries({ dataKey, className, strokeWidth = 2 }: ChartLineSeriesProps) {\n  const ctx = useChart(\"ChartLineSeries\");\n  const points = seriesPoints(ctx, dataKey);\n  if (!points.length) return null;\n  const index = seriesIndex(ctx, dataKey);\n  const stroke = resolveSeriesColor(ctx.config, dataKey, index, \"stroke\");\n  const dot = resolveSeriesColor(ctx.config, dataKey, index, \"fill\");\n  const d = points.map((p, i) => `${i === 0 ? \"M\" : \"L\"}${p.x} ${p.y}`).join(\" \");\n  const active = activePointOf(ctx, points);\n  return (\n    <>\n      <ChartPath\n        d={d}\n        className={cn(stroke.className, className)}\n        stroke={stroke.color}\n        strokeWidth={strokeWidth}\n        fill=\"none\"\n      />\n      {active ? (\n        <ChartCircle cx={active.x} cy={active.y} r={4} className={dot.className} fill={dot.color} />\n      ) : null}\n    </>\n  );\n}\n\nexport type ChartAreaSeriesProps = {\n  dataKey: string;\n  className?: string;\n  strokeWidth?: number;\n};\n\nfunction ChartAreaSeries({ dataKey, className, strokeWidth = 2 }: ChartAreaSeriesProps) {\n  const ctx = useChart(\"ChartAreaSeries\");\n  const points = seriesPoints(ctx, dataKey);\n  if (!points.length) return null;\n  const { scale } = ctx;\n  const index = seriesIndex(ctx, dataKey);\n  const fill = resolveSeriesColor(ctx.config, dataKey, index, \"fill\");\n  const stroke = resolveSeriesColor(ctx.config, dataKey, index, \"stroke\");\n  const baseline = scale.plotTop + scale.plotHeight;\n  const first = points[0];\n  const last = points[points.length - 1];\n  const lineD = points.map((p, i) => `${i === 0 ? \"M\" : \"L\"}${p.x} ${p.y}`).join(\" \");\n  const areaD = `${lineD} L${last.x} ${baseline} L${first.x} ${baseline} Z`;\n  const active = activePointOf(ctx, points);\n  return (\n    <>\n      {/* 채움은 반투명（선 Path가 위에 그려진다） */}\n      <ChartPath\n        d={areaD}\n        className={cn(fill.className, \"opacity-50\", className)}\n        fill={fill.color}\n      />\n      <ChartPath\n        d={lineD}\n        className={stroke.className}\n        stroke={stroke.color}\n        strokeWidth={strokeWidth}\n        fill=\"none\"\n      />\n      {active ? (\n        // 활성 점 채움 — 선 계열과 동일하게 stroke 색(G3 F-15: fill 누락 보강)\n        <ChartCircle\n          cx={active.x}\n          cy={active.y}\n          r={4}\n          className={dotOf(ctx, dataKey)}\n          fill={stroke.color}\n        />\n      ) : null}\n    </>\n  );\n}\n\nfunction dotOf(ctx: ChartContextValue, dataKey: string): string {\n  return resolveSeriesColor(ctx.config, dataKey, seriesIndex(ctx, dataKey), \"fill\").className;\n}\n\n// ---------------------------------------------------------------------------\n// 격자축\n// ---------------------------------------------------------------------------\n\nexport type ChartGridProps = {\n  className?: string;\n};\n\nfunction ChartGrid({ className }: ChartGridProps) {\n  const { scale } = useChart(\"ChartGrid\");\n  const x2 = scale.plotLeft + scale.plotWidth;\n  return (\n    <>\n      {scale.ticksY.map((tick) => (\n        <ChartLine\n          key={tick}\n          x1={scale.plotLeft}\n          x2={x2}\n          y1={scale.yAt(tick)}\n          y2={scale.yAt(tick)}\n          strokeWidth={1}\n          className={cn(\"stroke-border/50\", className)}\n        />\n      ))}\n    </>\n  );\n}\n\nexport type ChartXAxisProps = {\n  className?: string;\n  /** x 라벨 포맷（기본: 데이터 문자열 그대로） */\n  tickFormat?: (value: string, index: number) => string;\n};\n\nfunction ChartXAxis({ className, tickFormat }: ChartXAxisProps) {\n  const ctx = useChart(\"ChartXAxis\");\n  const { scale, data, xKey } = ctx;\n  const baseline = scale.plotTop + scale.plotHeight;\n  // 라벨 겹침 방지 — 12px 폰트 기준 24px당 1라벨 이하로 스킵한다.\n  const step = Math.max(1, Math.ceil((data.length * 24) / Math.max(scale.plotWidth, 1)));\n  return (\n    <>\n      <ChartLine\n        x1={scale.plotLeft}\n        x2={scale.plotLeft + scale.plotWidth}\n        y1={baseline}\n        y2={baseline}\n        strokeWidth={1}\n        className={cn(\"stroke-border\", className)}\n      />\n      {data.map((datum, index) => {\n        if (index % step !== 0) return null;\n        const raw = datum[xKey];\n        const value = typeof raw === \"string\" ? raw : raw == null ? \"\" : String(raw);\n        return (\n          <ChartText\n            key={index}\n            x={scale.xAt(index)}\n            y={baseline + 16}\n            textAnchor=\"middle\"\n            fontSize={12}\n            className={cn(\"fill-muted-foreground\", className)}\n          >\n            {tickFormat ? tickFormat(value, index) : value}\n          </ChartText>\n        );\n      })}\n    </>\n  );\n}\n\nexport type ChartYAxisProps = {\n  className?: string;\n  /** y 틱 포맷（기본: toLocaleString） */\n  tickFormat?: (value: number) => string;\n};\n\nfunction ChartYAxis({ className, tickFormat }: ChartYAxisProps) {\n  const { scale } = useChart(\"ChartYAxis\");\n  return (\n    <>\n      <ChartLine\n        x1={scale.plotLeft}\n        x2={scale.plotLeft}\n        y1={scale.plotTop}\n        y2={scale.plotTop + scale.plotHeight}\n        strokeWidth={1}\n        className={cn(\"stroke-border\", className)}\n      />\n      {scale.ticksY.map((tick) => (\n        <ChartText\n          key={tick}\n          x={scale.plotLeft - 8}\n          y={scale.yAt(tick) + 4}\n          textAnchor=\"end\"\n          fontSize={12}\n          className={cn(\"fill-muted-foreground\", className)}\n        >\n          {tickFormat ? tickFormat(tick) : formatChartValue(tick)}\n        </ChartText>\n      ))}\n    </>\n  );\n}\n\n// ---------------------------------------------------------------------------\n// 툴팁 — 웹 hover（ChartSurface onPointMove）탭（투명 Rect onPress）\n// ---------------------------------------------------------------------------\n\nexport type ChartTooltipProps = {\n  /** 커스텀 툴팁 콘텐츠（미지정 시 ChartTooltipContent） */\n  content?: ReactNode;\n};\n\nfunction ChartTooltip({ content }: ChartTooltipProps) {\n  const ctx = useChart(\"ChartTooltip\");\n  if (ctx.activeIndex == null) return null;\n  const x = ctx.scale.xAt(ctx.activeIndex);\n  // 카드 폭 측정 불가 — min-w-32（128px） 가정 좌우 클램프（근사）.\n  const left = Math.min(Math.max(x - TOOLTIP_WIDTH / 2, 0), Math.max(ctx.width - TOOLTIP_WIDTH, 0));\n  return (\n    <View pointerEvents=\"none\" className=\"absolute\" style={{ left: Math.round(left), top: 0 }}>\n      {content ?? <ChartTooltipContent />}\n    </View>\n  );\n}\n\nexport type ChartTooltipFormatter = (value: string | number, name: string) => ReactNode;\nexport type ChartTooltipLabelFormatter = (label: string, datum: ChartDatum) => ReactNode;\n\nexport type ChartTooltipContentProps = {\n  className?: string;\n  hideLabel?: boolean;\n  hideIndicator?: boolean;\n  indicator?: \"dot\" | \"line\" | \"dashed\";\n  labelFormatter?: ChartTooltipLabelFormatter;\n  labelClassName?: string;\n  formatter?: ChartTooltipFormatter;\n  /** 인디케이터 색 오버라이드（CSS 색 문자열） */\n  color?: string;\n  /** 항목 이름을 config에서 조회할 키（기본: 시리즈 dataKey） */\n  nameKey?: string;\n  /** 라벨 값을 datum에서 읽을 키（기본: xKey） */\n  labelKey?: string;\n};\n\nfunction ChartTooltipContent({\n  className,\n  indicator = \"dot\",\n  hideLabel = false,\n  hideIndicator = false,\n  labelFormatter,\n  labelClassName,\n  formatter,\n  color,\n  nameKey,\n  labelKey,\n}: ChartTooltipContentProps) {\n  const ctx = useChart(\"ChartTooltipContent\");\n  // 활성 행 — recharts payload 대신 context에서 얻는다（머리주석 참고）.\n  const datum = ctx.activeIndex == null ? undefined : ctx.data[ctx.activeIndex];\n  if (!datum) return null;\n\n  const rawLabel = labelKey ? datum[labelKey] : datum[ctx.xKey];\n  const labelConfig = labelKey ? ctx.config[labelKey]?.label : ctx.config[ctx.xKey]?.label;\n  const labelText =\n    typeof labelConfig === \"string\" ? labelConfig : rawLabel == null ? \"\" : String(rawLabel);\n\n  const items = ctx.seriesKeys\n    .map((key) => {\n      const value = datum[key];\n      if (value == null) return null;\n      const itemConfig = ctx.config[nameKey ?? key] ?? ctx.config[key];\n      const resolved = resolveSeriesColor(ctx.config, key, seriesIndex(ctx, key), \"bg\");\n      return { key, value, itemConfig, colorClass: resolved.className, colorValue: resolved.color };\n    })\n    .filter((item): item is NonNullable<typeof item> => item !== null);\n\n  const nestLabel = items.length === 1 && indicator !== \"dot\";\n\n  const tooltipLabel =\n    hideLabel || !labelText ? null : (\n      <View className={cn(labelClassName)}>\n        {labelFormatter ? (\n          labelFormatter(labelText, datum)\n        ) : (\n          <Text className=\"font-sans text-xs font-medium text-foreground\">{labelText}</Text>\n        )}\n      </View>\n    );\n\n  return (\n    <View\n      className={cn(\n        \"grid min-w-32 items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 web:shadow-xl\",\n        className,\n      )}\n    >\n      {!nestLabel ? tooltipLabel : null}\n      <View className=\"grid gap-1.5\">\n        {items.map((item) => {\n          const label = item.itemConfig?.label;\n          const name = typeof label === \"string\" ? label : item.key;\n          const indicatorColor = color ?? item.colorValue;\n          return (\n            <View\n              key={item.key}\n              className={cn(\n                \"flex w-full flex-row flex-wrap items-stretch gap-2\",\n                indicator === \"dot\" && \"items-center\",\n              )}\n            >\n              {formatter && item.value !== undefined ? (\n                formatter(item.value, name)\n              ) : (\n                <>\n                  {item.itemConfig?.icon ? (\n                    <item.itemConfig.icon />\n                  ) : (\n                    !hideIndicator && (\n                      <View\n                        className={cn(\n                          \"shrink-0 rounded-xs\",\n                          indicator === \"dot\" && \"h-2.5 w-2.5\",\n                          indicator === \"line\" && \"w-1\",\n                          indicator === \"dashed\" && \"w-0 border-2 border-dashed bg-transparent\",\n                          nestLabel && indicator === \"dashed\" && \"my-0.5\",\n                          !indicatorColor && item.colorClass,\n                        )}\n                        style={\n                          indicatorColor\n                            ? { backgroundColor: indicatorColor, borderColor: indicatorColor }\n                            : undefined\n                        }\n                      />\n                    )\n                  )}\n                  <View\n                    className={cn(\n                      \"flex flex-1 flex-row justify-between leading-none\",\n                      nestLabel ? \"items-end\" : \"items-center\",\n                    )}\n                  >\n                    <View className=\"grid gap-1.5\">\n                      {nestLabel ? tooltipLabel : null}\n                      <Text className=\"text-xs text-muted-foreground\">{name}</Text>\n                    </View>\n                    {item.value != null && (\n                      <Text className=\"font-mono text-xs font-medium text-foreground tabular-nums\">\n                        {typeof item.value === \"number\"\n                          ? formatChartValue(item.value)\n                          : String(item.value)}\n                      </Text>\n                    )}\n                  </View>\n                </>\n              )}\n            </View>\n          );\n        })}\n      </View>\n    </View>\n  );\n}\n\n// ---------------------------------------------------------------------------\n// 범례 — 컨테이너 흐름（SVG 밖）에 렌더된다\n// ---------------------------------------------------------------------------\n\nexport type ChartLegendItem = {\n  dataKey: string;\n  /** CSS 색 문자열（미지정 시 순번 토큰 클래스 bg-chart-N） */\n  color?: string;\n};\n\nexport type ChartLegendProps = {\n  content?: ReactNode;\n  verticalAlign?: \"top\" | \"bottom\";\n  className?: string;\n};\n\nfunction ChartLegend({ content, verticalAlign = \"bottom\", className }: ChartLegendProps) {\n  const ctx = useChart(\"ChartLegend\");\n  if (content != null) return <>{content}</>;\n  return (\n    <ChartLegendContent\n      payload={defaultLegendPayload(ctx)}\n      verticalAlign={verticalAlign}\n      className={className}\n    />\n  );\n}\n\nfunction defaultLegendPayload(ctx: ChartContextValue): ChartLegendItem[] {\n  return ctx.seriesKeys.map((key) => ({\n    dataKey: key,\n    color: resolveSeriesColor(ctx.config, key, seriesIndex(ctx, key), \"bg\").color,\n  }));\n}\n\nexport type ChartLegendContentProps = {\n  className?: string;\n  hideIcon?: boolean;\n  payload?: ChartLegendItem[];\n  verticalAlign?: \"top\" | \"bottom\";\n  nameKey?: string;\n};\n\nfunction ChartLegendContent({\n  className,\n  hideIcon = false,\n  payload,\n  verticalAlign = \"bottom\",\n  nameKey,\n}: ChartLegendContentProps) {\n  const ctx = useChart(\"ChartLegendContent\");\n  const items = payload ?? defaultLegendPayload(ctx);\n  if (!items.length) return null;\n  return (\n    <View\n      className={cn(\n        \"w-full flex-row items-center justify-center gap-4\",\n        verticalAlign === \"top\" ? \"pb-3\" : \"pt-3\",\n        className,\n      )}\n    >\n      {items.map((item) => {\n        const itemConfig = ctx.config[nameKey ?? item.dataKey] ?? ctx.config[item.dataKey];\n        const label = itemConfig?.label;\n        const colorClass = item.color ? \"\" : getChartColor(seriesIndex(ctx, item.dataKey), \"bg\");\n        return (\n          <View key={item.dataKey} className=\"flex-row items-center gap-1.5\">\n            {itemConfig?.icon && !hideIcon ? (\n              <itemConfig.icon />\n            ) : (\n              <View\n                className={cn(\"h-2 w-2 shrink-0 rounded-xs\", colorClass)}\n                style={item.color ? { backgroundColor: item.color } : undefined}\n              />\n            )}\n            {typeof label === \"string\" ? (\n              <Text className=\"text-xs text-foreground\">{label}</Text>\n            ) : label != null ? (\n              label\n            ) : (\n              <Text className=\"text-xs text-foreground\">{item.dataKey}</Text>\n            )}\n          </View>\n        );\n      })}\n    </View>\n  );\n}\n\n// ---------------------------------------------------------------------------\n// ChartStyle — 원본 API 유지용 no-op（머리주석 참고）\n// ---------------------------------------------------------------------------\n\nfunction ChartStyle(_props: { id?: string; config?: ChartConfig }) {\n  return null;\n}\n\n/**\n * fluid 캔버스 폭 측정 — width=\"fluid\"면 컨테이너 자체 폭（ResizeObserver）을\n * 캔버스 폭으로 쓴다. 첫 측정 전엔 기본 320으로 렌더해 전환한다. 네이티브는\n * ResizeObserver 계약이 없어 기본값으로 폴백한다（고정 폭 사용 권장）.\n */\nfunction useFluidWidth(\n  ref: RefObject<HTMLElement | null>,\n  width: number | \"fluid\",\n): number {\n  const [measured, setMeasured] = useState(0);\n  useEffect(() => {\n    if (width !== \"fluid\") return;\n    const el = ref.current;\n    if (!el || typeof ResizeObserver !== \"function\") return;\n    const observer = new ResizeObserver((entries) => {\n      const next = Math.round(entries[0]?.contentRect.width ?? 0);\n      setMeasured((prev) => (Math.abs(prev - next) >= 1 ? next : prev));\n    });\n    observer.observe(el);\n    return () => observer.disconnect();\n  }, [ref, width]);\n  return width === \"fluid\" ? measured || INITIAL_DIMENSION.width : width;\n}\n\n// ---------------------------------------------------------------------------\n// 컨테이너 — context 제공children 분류포인터 이벤트\n// ---------------------------------------------------------------------------\n\nexport type ChartContainerProps = Omit<ViewProps, \"children\"> & {\n  config: ChartConfig;\n  data?: ChartDatum[];\n  /** x축（범주） 데이터 키 — 미지정 시 첫 행의 첫 문자열 값 키 */\n  xKey?: string;\n  /** 캔버스 폭（px） — 기본 320（원본 INITIAL_DIMENSION）. \"fluid\"면 컨테이너\n   *  폭에 맞춘다（웹 전용 — ResizeObserver, 네이티브는 기본값 폴백） */\n  width?: number | \"fluid\";\n  /** 캔버스 높이（px） — 기본 200 */\n  height?: number;\n  /** 시리즈 요약 accessibilityLabel 덮어쓰기 */\n  accessibilityLabel?: string;\n  children?: ReactNode;\n};\n\nconst ChartContainer = forwardRef<BridgeViewRef, ChartContainerProps>(\n  (\n    {\n      config,\n      data = [],\n      xKey,\n      width = INITIAL_DIMENSION.width,\n      height = INITIAL_DIMENSION.height,\n      accessibilityLabel,\n      className,\n      children,\n      ...props\n    },\n    ref,\n  ) => {\n    const [activeIndex, setActiveIndex] = useState<number | null>(null);\n\n    // fluid 캔버스 측정 ref — forwardRef 노출과 겸한다（브리지 ref 인스턴스는\n    // 웹에서 div라 HTMLElement로 캐스팅해 쓴다）.\n    const containerRef = useRef<HTMLElement | null>(null);\n    const canvasWidth = useFluidWidth(containerRef, width);\n    const setContainerRef = (instance: BridgeViewRef | null) => {\n      containerRef.current = instance as HTMLElement | null;\n      if (typeof ref === \"function\") ref(instance);\n      else if (ref) (ref as { current: BridgeViewRef | null }).current = instance;\n    };\n\n    // children 분류 — SVG 조각（시리즈축격자）, 오버레이（ChartTooltip）,\n    // 흐름（ChartLegend）. 시리즈는 색 인덱스 순서 확보를 위해 dataKey를 수집한다.\n    const svgChildren: ReactNode[] = [];\n    const overlayChildren: ReactNode[] = [];\n    const flowChildren: ReactNode[] = [];\n    const seriesKeys: string[] = [];\n    Children.forEach(children, (child) => {\n      if (isValidElement(child)) {\n        if (child.type === ChartTooltip) {\n          overlayChildren.push(child);\n          return;\n        }\n        if (child.type === ChartLegend) {\n          flowChildren.push(child);\n          return;\n        }\n        if (\n          child.type === ChartBarSeries ||\n          child.type === ChartLineSeries ||\n          child.type === ChartAreaSeries\n        ) {\n          const dataKey = (child.props as { dataKey?: string }).dataKey;\n          if (typeof dataKey === \"string\") seriesKeys.push(dataKey);\n        }\n      }\n      svgChildren.push(child);\n    });\n\n    const resolvedXKey = xKey ?? firstCategoryKey(data);\n    const scale = useMemo(\n      () => buildScale(data, seriesKeys, canvasWidth, height),\n      // eslint-disable-next-line react-hooks/exhaustive-deps -- seriesKeys는 children에서 유도\n      [children, data, canvasWidth, height],\n    );\n\n    /** SVG x좌표 → 가장 가까운 데이터 행 인덱스 */\n    const nearestIndex = (x: number): number | null => {\n      if (!data.length) return null;\n      const index = Math.round((x - scale.plotLeft) / scale.bandWidth - 0.5);\n      return Math.min(data.length - 1, Math.max(0, index));\n    };\n\n    const summary =\n      accessibilityLabel ?? summarizeSeries(config, data, resolvedXKey, seriesKeys);\n\n    return (\n      <ChartContext.Provider\n        value={{\n          config,\n          data,\n          xKey: resolvedXKey,\n          scale,\n          seriesKeys,\n          activeIndex,\n          width: canvasWidth,\n          height,\n        }}\n      >\n        <View\n          ref={setContainerRef}\n          className={cn(\n            \"relative flex-col items-center\",\n            width === \"fluid\" && \"w-full\",\n            className,\n          )}\n          {...props}\n        >\n          <View className=\"relative\">\n            <ChartSurface\n              width={canvasWidth}\n              height={height}\n              accessibilityLabel={summary}\n              onPointMove={(point) => setActiveIndex(nearestIndex(point.x))}\n              onPointLeave={() => setActiveIndex(null)}\n            >\n              {svgChildren}\n              {/* 투명 캡처층 — 웹 클릭네이티브 탭으로 툴팁을 띄운다（재탭 닫기） */}\n              <ChartRect\n                x={0}\n                y={0}\n                width={canvasWidth}\n                height={height}\n                fill=\"transparent\"\n                onPress={(point) => {\n                  const next = nearestIndex(point.x);\n                  setActiveIndex((prev) => (prev === next ? null : next));\n                }}\n              />\n            </ChartSurface>\n            {/* 툴팁 오버레이 — 포인터는 통과（box-nonenone）시켜 hover가 끊기지 않게 한다 */}\n            <View pointerEvents=\"box-none\" className=\"absolute inset-0\">\n              {overlayChildren}\n            </View>\n          </View>\n          {flowChildren}\n        </View>\n      </ChartContext.Provider>\n    );\n  },\n);\nChartContainer.displayName = \"ChartContainer\";\n\nexport {\n  ChartContainer,\n  ChartStyle,\n  ChartTooltip,\n  ChartTooltipContent,\n  ChartLegend,\n  ChartLegendContent,\n  ChartBarSeries,\n  ChartLineSeries,\n  ChartAreaSeries,\n  ChartGrid,\n  ChartXAxis,\n  ChartYAxis,\n  getChartColor,\n};\n",
      "type": "registry:ui",
      "target": "components/ui/chart.tsx"
    }
  ],
  "type": "registry:ui"
}
