{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "charts",
  "title": "Charts",
  "description": "Render static AI-provided chart payloads using shadcn chart conventions and Recharts.",
  "dependencies": [
    "ai",
    "zod",
    "recharts"
  ],
  "registryDependencies": [
    "alert",
    "card",
    "badge",
    "chart",
    "empty",
    "skeleton",
    "tabs",
    "shobky/fable-ui/core"
  ],
  "files": [
    {
      "path": "components/fable-ui/charts/index.ts",
      "content": "export * from \"./charts\"\nexport * from \"./charts.types\"\nexport * from \"./tool-definition\"\n",
      "type": "registry:block",
      "target": "@components/fable-ui/charts/index.ts"
    },
    {
      "path": "components/fable-ui/charts/charts.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  Bar,\n  BarChart,\n  CartesianGrid,\n  Cell,\n  Line,\n  LineChart,\n  Pie,\n  PieChart,\n  XAxis,\n  YAxis,\n} from \"recharts\"\n\nimport { Alert, AlertDescription, AlertTitle } from \"@/components/ui/alert\"\nimport { Badge } from \"@/components/ui/badge\"\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\"\nimport {\n  ChartContainer,\n  ChartLegend,\n  ChartLegendContent,\n  ChartTooltip,\n  ChartTooltipContent,\n  type ChartConfig,\n} from \"@/components/ui/chart\"\nimport {\n  Empty,\n  EmptyDescription,\n  EmptyHeader,\n  EmptyTitle,\n} from \"@/components/ui/empty\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport { Tabs, TabsList, TabsTrigger } from \"@/components/ui/tabs\"\nimport { cn } from \"@/lib/utils\"\nimport { chartTypes, type ChartsProps, type ChartType } from \"./charts.types\"\n\nconst chartTypeLabels: Record<ChartType, string> = {\n  line: \"Line\",\n  bar: \"Bar\",\n  pie: \"Pie\",\n}\n\nconst chartColors = [\n  \"var(--chart-1)\",\n  \"var(--chart-2)\",\n  \"var(--chart-3)\",\n  \"var(--chart-4)\",\n  \"var(--chart-5)\",\n]\n\ntype ResolvedSeries = {\n  key: string\n  label: string\n  color: string\n}\n\ntype ChartProblem = {\n  title: string\n  description?: string\n}\n\nfunction normalizeChartTypes(types?: ChartType[]): ChartType[] {\n  const uniqueTypes = new Set(\n    types?.filter((type) => chartTypes.includes(type))\n  )\n\n  return uniqueTypes.size > 0 ? Array.from(uniqueTypes) : [\"line\"]\n}\n\nfunction isFiniteNumber(value: unknown): value is number {\n  return typeof value === \"number\" && Number.isFinite(value)\n}\n\nfunction toFiniteNumber(value: unknown) {\n  if (isFiniteNumber(value)) {\n    return value\n  }\n\n  if (typeof value === \"string\" && value.trim() !== \"\") {\n    const next = Number(value)\n    return Number.isFinite(next) ? next : undefined\n  }\n\n  return undefined\n}\n\nfunction getRowKeys(data: ChartsProps[\"data\"]) {\n  const keys = new Set<string>()\n\n  for (const row of data) {\n    Object.keys(row).forEach((key) => keys.add(key))\n  }\n\n  return Array.from(keys)\n}\n\nfunction getNumericKeys(data: ChartsProps[\"data\"], keys: string[]) {\n  return keys.filter((key) =>\n    data.some((row) => toFiniteNumber(row[key]) !== undefined)\n  )\n}\n\nfunction getTextKeys(data: ChartsProps[\"data\"], keys: string[]) {\n  return keys.filter((key) =>\n    data.some((row) => {\n      const value = row[key]\n      return typeof value === \"string\" && value.trim() !== \"\"\n    })\n  )\n}\n\nfunction titleizeKey(key: string) {\n  return key\n    .replace(/[_-]+/g, \" \")\n    .replace(/([a-z])([A-Z])/g, \"$1 $2\")\n    .replace(/\\s+/g, \" \")\n    .trim()\n    .replace(/^./, (char) => char.toUpperCase())\n}\n\nfunction resolveSeries({\n  data,\n  xKey,\n  series,\n}: Pick<ChartsProps, \"data\" | \"xKey\" | \"series\">) {\n  const keys = getRowKeys(data)\n  const numericKeys = getNumericKeys(data, keys).filter((key) => key !== xKey)\n  const requestedSeries = series?.filter((item) =>\n    numericKeys.includes(item.key)\n  )\n  const sourceSeries: Array<{\n    key: string\n    label?: string\n    color?: string\n  }> =\n    requestedSeries && requestedSeries.length > 0\n      ? requestedSeries\n      : numericKeys.slice(0, 4).map((key) => ({ key }))\n\n  return sourceSeries.map((item, index) => ({\n    key: item.key,\n    label: item.label || titleizeKey(item.key),\n    color: item.color || chartColors[index % chartColors.length],\n  }))\n}\n\nfunction resolveCartesianKeys(props: ChartsProps) {\n  const keys = getRowKeys(props.data)\n  const textKeys = getTextKeys(props.data, keys)\n  const numericKeys = getNumericKeys(props.data, keys)\n  const xKey =\n    props.xKey && keys.includes(props.xKey)\n      ? props.xKey\n      : (textKeys[0] ?? keys.find((key) => !numericKeys.includes(key)))\n  const series = resolveSeries({ ...props, xKey })\n\n  return { xKey, series }\n}\n\nfunction resolvePieKeys(props: ChartsProps) {\n  const keys = getRowKeys(props.data)\n  const textKeys = getTextKeys(props.data, keys)\n  const numericKeys = getNumericKeys(props.data, keys)\n  const categoryKey =\n    props.categoryKey && keys.includes(props.categoryKey)\n      ? props.categoryKey\n      : (textKeys[0] ?? keys.find((key) => key !== props.valueKey))\n  const valueKey =\n    props.valueKey && numericKeys.includes(props.valueKey)\n      ? props.valueKey\n      : numericKeys.find((key) => key !== categoryKey)\n\n  return { categoryKey, valueKey }\n}\n\nfunction getChartProblem(\n  type: ChartType,\n  props: ChartsProps\n): ChartProblem | null {\n  if (props.data.length === 0) {\n    return null\n  }\n\n  if (type === \"pie\") {\n    const { categoryKey, valueKey } = resolvePieKeys(props)\n\n    if (!categoryKey || !valueKey) {\n      return {\n        title: \"Chart unavailable\",\n        description:\n          \"Pie charts need a category key and a numeric value key in the static data payload.\",\n      }\n    }\n\n    return null\n  }\n\n  const { xKey, series } = resolveCartesianKeys(props)\n\n  if (!xKey || series.length === 0) {\n    return {\n      title: \"Chart unavailable\",\n      description:\n        \"Line and bar charts need an x key and at least one numeric series in the static data payload.\",\n    }\n  }\n\n  return null\n}\n\nfunction createFormatter(format: ChartsProps[\"format\"]) {\n  return (value: unknown) => {\n    const numericValue = toFiniteNumber(value)\n\n    if (numericValue === undefined) {\n      return String(value ?? \"\")\n    }\n\n    if (format?.value === \"percent\") {\n      return new Intl.NumberFormat(format.locale, {\n        style: \"percent\",\n        maximumFractionDigits: 1,\n      }).format(numericValue)\n    }\n\n    if (format?.value === \"currency\") {\n      return new Intl.NumberFormat(format.locale, {\n        style: \"currency\",\n        currency: format.currency || \"USD\",\n        notation: format.compact ? \"compact\" : \"standard\",\n        maximumFractionDigits: format.compact ? 1 : 2,\n      }).format(numericValue)\n    }\n\n    return new Intl.NumberFormat(format?.locale, {\n      notation: format?.compact ? \"compact\" : \"standard\",\n      maximumFractionDigits: 2,\n    }).format(numericValue)\n  }\n}\n\nfunction toChartConfig(series: ResolvedSeries[]): ChartConfig {\n  return Object.fromEntries(\n    series.map((item) => [\n      item.key,\n      {\n        label: item.label,\n        color: item.color,\n      },\n    ])\n  )\n}\n\nfunction toCartesianData(\n  data: ChartsProps[\"data\"],\n  xKey: string,\n  series: ResolvedSeries[]\n) {\n  return data.map((row) => {\n    const next: Record<string, string | number | null> = {\n      [xKey]: String(row[xKey] ?? \"\"),\n    }\n\n    for (const item of series) {\n      next[item.key] = toFiniteNumber(row[item.key]) ?? null\n    }\n\n    return next\n  })\n}\n\nfunction toPieData(\n  data: ChartsProps[\"data\"],\n  categoryKey: string,\n  valueKey: string\n) {\n  return data\n    .map((row, index) => ({\n      name: String(row[categoryKey] ?? `Slice ${index + 1}`),\n      value: toFiniteNumber(row[valueKey]) ?? 0,\n      fill: chartColors[index % chartColors.length],\n    }))\n    .filter((row) => row.value > 0)\n}\n\nfunction ChartLoading() {\n  return (\n    <div className=\"flex flex-col gap-3\">\n      <Skeleton className=\"h-72 w-full rounded-lg\" />\n      <div className=\"flex justify-center gap-3\">\n        <Skeleton className=\"h-4 w-20\" />\n        <Skeleton className=\"h-4 w-24\" />\n      </div>\n    </div>\n  )\n}\n\nfunction ChartEmpty({ emptyState }: Pick<ChartsProps, \"emptyState\">) {\n  return (\n    <Empty className=\"min-h-72 rounded-lg border\">\n      <EmptyHeader>\n        <EmptyTitle>{emptyState?.title || \"No chart data\"}</EmptyTitle>\n        {emptyState?.description ? (\n          <EmptyDescription>{emptyState.description}</EmptyDescription>\n        ) : null}\n      </EmptyHeader>\n    </Empty>\n  )\n}\n\nfunction ChartError({ error }: { error: ChartProblem }) {\n  return (\n    <Alert variant=\"destructive\">\n      <AlertTitle>{error.title}</AlertTitle>\n      {error.description ? (\n        <AlertDescription>{error.description}</AlertDescription>\n      ) : null}\n    </Alert>\n  )\n}\n\nfunction CartesianChart({\n  type,\n  props,\n}: {\n  type: \"line\" | \"bar\"\n  props: ChartsProps\n}) {\n  const { xKey, series } = resolveCartesianKeys(props)\n  const formatValue = createFormatter(props.format)\n\n  if (!xKey || series.length === 0) {\n    return null\n  }\n\n  const chartData = toCartesianData(props.data, xKey, series)\n  const config = toChartConfig(series)\n\n  return (\n    <ChartContainer\n      config={config}\n      className=\"min-h-72 w-full sm:min-h-80\"\n      initialDimension={{ width: 520, height: 320 }}\n    >\n      {type === \"line\" ? (\n        <LineChart\n          accessibilityLayer\n          data={chartData}\n          margin={{ left: 8, right: 8 }}\n        >\n          <CartesianGrid vertical={false} />\n          <XAxis\n            dataKey={xKey}\n            tickLine={false}\n            axisLine={false}\n            tickMargin={8}\n          />\n          <YAxis\n            tickLine={false}\n            axisLine={false}\n            tickMargin={8}\n            tickFormatter={formatValue}\n            width={48}\n          />\n          <ChartTooltip content={<ChartTooltipContent indicator=\"line\" />} />\n          <ChartLegend content={<ChartLegendContent />} />\n          {series.map((item) => (\n            <Line\n              key={item.key}\n              dataKey={item.key}\n              name={item.label}\n              type=\"monotone\"\n              stroke={`var(--color-${item.key})`}\n              strokeWidth={2}\n              dot={false}\n              activeDot={{ r: 4 }}\n            />\n          ))}\n        </LineChart>\n      ) : (\n        <BarChart\n          accessibilityLayer\n          data={chartData}\n          margin={{ left: 8, right: 8 }}\n        >\n          <CartesianGrid vertical={false} />\n          <XAxis\n            dataKey={xKey}\n            tickLine={false}\n            axisLine={false}\n            tickMargin={8}\n          />\n          <YAxis\n            tickLine={false}\n            axisLine={false}\n            tickMargin={8}\n            tickFormatter={formatValue}\n            width={48}\n          />\n          <ChartTooltip content={<ChartTooltipContent indicator=\"dot\" />} />\n          <ChartLegend content={<ChartLegendContent />} />\n          {series.map((item) => (\n            <Bar\n              key={item.key}\n              dataKey={item.key}\n              name={item.label}\n              fill={`var(--color-${item.key})`}\n              radius={[4, 4, 0, 0]}\n            />\n          ))}\n        </BarChart>\n      )}\n    </ChartContainer>\n  )\n}\n\nfunction PieChartView({ props }: { props: ChartsProps }) {\n  const { categoryKey, valueKey } = resolvePieKeys(props)\n  const formatValue = createFormatter(props.format)\n\n  if (!categoryKey || !valueKey) {\n    return null\n  }\n\n  const chartData = toPieData(props.data, categoryKey, valueKey)\n  const config: ChartConfig = {\n    [valueKey]: {\n      label: titleizeKey(valueKey),\n      color: chartColors[0],\n    },\n  }\n\n  return (\n    <ChartContainer\n      config={config}\n      className=\"mx-auto min-h-72 w-full max-w-xl sm:min-h-80\"\n      initialDimension={{ width: 520, height: 320 }}\n    >\n      <PieChart accessibilityLayer>\n        <ChartTooltip\n          content={\n            <ChartTooltipContent\n              hideLabel\n              nameKey=\"name\"\n              formatter={(value, name) => (\n                <div className=\"flex flex-1 items-center justify-between gap-4 leading-none\">\n                  <span className=\"text-muted-foreground\">{String(name)}</span>\n                  <span className=\"font-mono font-medium text-foreground tabular-nums\">\n                    {formatValue(value)}\n                  </span>\n                </div>\n              )}\n            />\n          }\n        />\n        <Pie\n          data={chartData}\n          dataKey=\"value\"\n          nameKey=\"name\"\n          innerRadius=\"48%\"\n          outerRadius=\"78%\"\n          paddingAngle={2}\n        >\n          {chartData.map((entry) => (\n            <Cell key={entry.name} fill={entry.fill} />\n          ))}\n        </Pie>\n        <ChartLegend content={<ChartLegendContent nameKey=\"name\" />} />\n      </PieChart>\n    </ChartContainer>\n  )\n}\n\nfunction ChartBody({ type, props }: { type: ChartType; props: ChartsProps }) {\n  if (props.isLoading) {\n    return <ChartLoading />\n  }\n\n  if (props.error) {\n    return <ChartError error={props.error} />\n  }\n\n  if (props.data.length === 0) {\n    return <ChartEmpty emptyState={props.emptyState} />\n  }\n\n  const problem = getChartProblem(type, props)\n\n  if (problem) {\n    return <ChartError error={problem} />\n  }\n\n  return type === \"pie\" ? (\n    <PieChartView props={props} />\n  ) : (\n    <CartesianChart type={type} props={props} />\n  )\n}\n\nexport function Charts(props: ChartsProps) {\n  const {\n    title,\n    description,\n    context,\n    availableChartTypes,\n    defaultChartType,\n    isDisabled,\n  } = props\n  const resolvedChartTypes = normalizeChartTypes(availableChartTypes)\n  const initialChartType = defaultChartType\n    ? resolvedChartTypes.find((type) => type === defaultChartType)\n    : resolvedChartTypes[0]\n  const [selectedChartType, setSelectedChartType] = React.useState<ChartType>(\n    initialChartType ?? \"line\"\n  )\n  const activeChartType = resolvedChartTypes.includes(selectedChartType)\n    ? selectedChartType\n    : (initialChartType ?? resolvedChartTypes[0] ?? \"line\")\n\n  return (\n    <Card\n      className={cn(\"w-full max-w-3xl\", isDisabled && \"opacity-60\")}\n      data-fable-ui=\"charts\"\n      aria-busy={props.isLoading || undefined}\n    >\n      <CardHeader>\n        <div className=\"flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between\">\n          <div className=\"flex flex-col gap-1\">\n            <CardTitle className=\"text-base\">{title || \"Charts\"}</CardTitle>\n            {description ? (\n              <CardDescription>{description}</CardDescription>\n            ) : null}\n            {context ? (\n              <p className=\"text-sm text-muted-foreground\">{context}</p>\n            ) : null}\n          </div>\n          {resolvedChartTypes.length > 1 ? (\n            <Tabs\n              value={activeChartType}\n              onValueChange={(value) =>\n                setSelectedChartType(value as ChartType)\n              }\n            >\n              <TabsList aria-label=\"Chart type\">\n                {resolvedChartTypes.map((type) => (\n                  <TabsTrigger\n                    key={type}\n                    value={type}\n                    disabled={isDisabled || props.isLoading}\n                  >\n                    {chartTypeLabels[type]}\n                  </TabsTrigger>\n                ))}\n              </TabsList>\n            </Tabs>\n          ) : (\n            <Badge variant=\"secondary\">\n              {chartTypeLabels[activeChartType]}\n            </Badge>\n          )}\n        </div>\n      </CardHeader>\n      <CardContent>\n        <ChartBody type={activeChartType} props={props} />\n      </CardContent>\n    </Card>\n  )\n}\n",
      "type": "registry:block",
      "target": "@components/fable-ui/charts/charts.tsx"
    },
    {
      "path": "components/fable-ui/charts/charts.types.ts",
      "content": "export const chartTypes = [\"line\", \"bar\", \"pie\"] as const\n\nexport type ChartType = (typeof chartTypes)[number]\nexport type ChartValue = string | number | boolean | null\nexport type ChartDataRow = Record<string, ChartValue>\n\nexport type ChartSeries = {\n  key: string\n  label?: string\n  color?: string\n}\n\nexport type ChartFormat = {\n  value?: \"number\" | \"currency\" | \"percent\"\n  locale?: string\n  currency?: string\n  compact?: boolean\n}\n\nexport type ChartsProps = {\n  title: string\n  description?: string\n  context?: string\n  data: ChartDataRow[]\n  xKey?: string\n  categoryKey?: string\n  valueKey?: string\n  series?: ChartSeries[]\n  availableChartTypes?: ChartType[]\n  defaultChartType?: ChartType\n  format?: ChartFormat\n  emptyState?: {\n    title: string\n    description?: string\n  }\n  isLoading?: boolean\n  isDisabled?: boolean\n  error?: {\n    title: string\n    description?: string\n  }\n}\n",
      "type": "registry:block",
      "target": "@components/fable-ui/charts/charts.types.ts"
    },
    {
      "path": "components/fable-ui/charts/tool-definition.ts",
      "content": "import { lazy } from \"react\"\nimport { tool } from \"ai\"\nimport { z } from \"zod\"\n\nimport { defineFableComponent } from \"@/lib/fable-ui/core\"\nimport { chartTypes } from \"./charts.types\"\n\nconst Charts = lazy(() =>\n  import(\"./charts\").then((module) => ({ default: module.Charts })),\n)\n\nconst chartTypeSchema = z.enum(chartTypes)\nconst chartValueSchema = z.union([\n  z.string(),\n  z.number(),\n  z.boolean(),\n  z.null(),\n])\nconst chartDataRowSchema = z.record(z.string(), chartValueSchema)\n\nconst chartSeriesSchema = z.object({\n  key: z.string().min(1),\n  label: z.string().min(1).optional(),\n  color: z.string().min(1).optional(),\n})\n\nconst chartFormatSchema = z.object({\n  value: z.enum([\"number\", \"currency\", \"percent\"]).optional(),\n  locale: z.string().min(1).optional(),\n  currency: z.string().min(1).optional(),\n  compact: z.boolean().optional(),\n})\n\nexport const showChartInputSchema = z.object({\n  title: z.string().min(1),\n  description: z.string().optional(),\n  context: z.string().optional(),\n  data: z.array(chartDataRowSchema).max(200),\n  xKey: z.string().min(1).optional(),\n  categoryKey: z.string().min(1).optional(),\n  valueKey: z.string().min(1).optional(),\n  series: z.array(chartSeriesSchema).min(1).max(8).optional(),\n  availableChartTypes: z.array(chartTypeSchema).min(1).max(3).optional(),\n  defaultChartType: chartTypeSchema.optional(),\n  format: chartFormatSchema.optional(),\n  emptyState: z\n    .object({\n      title: z.string().min(1),\n      description: z.string().optional(),\n    })\n    .optional(),\n})\n\nexport type ShowChartInput = z.infer<typeof showChartInputSchema>\n\nexport function createShowChartTool() {\n  return tool({\n    description:\n      \"Show a chart from static, display-ready data already available in the conversation. Supports line, bar, and pie chart payloads. Do not fetch model-owned URLs, invent data, run SQL, or perform host data access.\",\n    inputSchema: showChartInputSchema,\n    execute: async (input) => input,\n  })\n}\n\nexport const showChartTool = createShowChartTool()\n\nexport const showChart = defineFableComponent({\n  name: \"show_chart\",\n  schema: showChartInputSchema,\n  tool: showChartTool,\n  renderer: {\n    Component: Charts,\n    loadingProps: { title: \"Charts\", data: [], isLoading: true },\n    emptyProps: { title: \"Charts\", data: [] },\n    errorProps: (description: string) => ({\n      title: \"Charts unavailable\",\n      data: [],\n      error: { title: \"Charts unavailable\", description },\n    }),\n    toProps: (data: ShowChartInput) => ({ ...data }),\n  },\n})\n",
      "type": "registry:block",
      "target": "@components/fable-ui/charts/tool-definition.ts"
    },
    {
      "path": "lib/fable-ui/tools/show-chart-tool.ts",
      "content": "export {\n  createShowChartTool,\n  showChart,\n  showChartInputSchema,\n  showChartTool,\n  type ShowChartInput,\n} from \"@/components/fable-ui/charts/tool-definition\"\n",
      "type": "registry:lib",
      "target": "@lib/fable-ui/tools/show-chart-tool.ts"
    },
    {
      "path": "lib/fable-ui/manifests/show-chart.md",
      "content": "---\ntool: show_chart\ncomponent: Charts\n---\n\n# show_chart\n\nUse `show_chart` when the assistant has static, display-ready data that is best understood as a line, bar, or pie chart.\n\nRules:\n\n- Keep model-provided chart data small and explicit.\n- Use `line` for trends over an ordered x-axis, `bar` for category comparison, and `pie` for part-to-whole slices.\n- Include `xKey` and `series` for line/bar charts.\n- Include `categoryKey` and `valueKey` for pie charts.\n- Set `availableChartTypes` when the same payload can be viewed in more than one chart type.\n- Do not let the model fetch URLs, query databases, run SQL, or decide authorization.\n- For host-owned data, ask the host application to provide validated rows before calling this tool.\n\nRendering uses shadcn chart conventions on top of Recharts. Static model-provided rows are supported; developer-owned data fetching should happen outside the tool payload and pass validated rows into the component.\n",
      "type": "registry:file",
      "target": "@lib/fable-ui/manifests/show-chart.md"
    }
  ],
  "type": "registry:block"
}