{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "trend-chart",
  "title": "Trend Chart",
  "description": "Area, line, or bar chart over time with tooltips and legend, built on shadcn's chart primitives.",
  "dependencies": [
    "recharts"
  ],
  "registryDependencies": [
    "chart"
  ],
  "files": [
    {
      "path": "registry/dashboardcn/ui/trend-chart.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  Area,\n  AreaChart,\n  AreaRevealShape,\n  Bar,\n  BarChart,\n  CartesianGrid,\n  Line,\n  LineChart,\n  ReferenceLine,\n  XAxis,\n  YAxis,\n  type AreaRevealShapeProps,\n} from \"recharts\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n  ChartContainer,\n  ChartLegend,\n  ChartLegendContent,\n  ChartTooltip,\n  ChartTooltipContent,\n  type ChartConfig,\n} from \"@/components/ui/chart\"\n\nexport interface TrendSeries {\n  /** Key in each data row. */\n  key: string\n  label: string\n  /** Any CSS color. Defaults to chart-1 through chart-5 in order. */\n  color?: string\n}\n\nexport interface TrendChartProps\n  extends Omit<React.ComponentProps<typeof ChartContainer>, \"config\" | \"children\"> {\n  data: Record<string, unknown>[]\n  /** Key of the x-axis value in each row. */\n  xKey: string\n  series: TrendSeries[]\n  type?: \"area\" | \"line\" | \"bar\"\n  /** Area fill: a plain gradient, or a dot grid that fades out toward the line. */\n  fill?: \"gradient\" | \"dots\"\n  /** Bars only: \"horizontal\" runs bars left to right with categories down the side. */\n  layout?: \"vertical\" | \"horizontal\"\n  /** Bars only: corner radius in px, or \"full\" for pill-shaped bars. */\n  barRadius?: number | \"full\"\n  /** Bars only: thickness of each bar in px. Defaults to a share of the category width. */\n  barSize?: number\n  stacked?: boolean\n  showLegend?: boolean\n  legendPosition?: \"top\" | \"bottom\"\n  legendAlign?: \"left\" | \"center\" | \"right\"\n  showGrid?: boolean\n  /** Show the value axis. In the horizontal layout this is the axis along the bottom. */\n  showYAxis?: boolean\n  /** Recharts domain for the value axis, e.g. [\"dataMin\", \"dataMax\"] or [\"auto\", \"auto\"]. Defaults to [0, \"auto\"]. */\n  yDomain?: React.ComponentProps<typeof YAxis>[\"domain\"]\n  xFormatter?: (value: unknown) => string\n  yFormatter?: (value: number) => string\n  /** Animate series after hydration and when data changes. Respects reduced motion. */\n  animate?: boolean\n  animationDuration?: number\n}\n\nconst reducedMotionQuery = \"(prefers-reduced-motion: reduce)\"\n\nfunction subscribeToReducedMotion(onChange: () => void) {\n  const query = window.matchMedia(reducedMotionQuery)\n  query.addEventListener(\"change\", onChange)\n  return () => query.removeEventListener(\"change\", onChange)\n}\n\nfunction getReducedMotionSnapshot() {\n  return window.matchMedia(reducedMotionQuery).matches\n}\n\nfunction getReducedMotionServerSnapshot() {\n  return true\n}\n\nfunction defaultXFormatter(value: unknown) {\n  if (typeof value === \"string\" && /^\\d{4}-\\d{2}-\\d{2}/.test(value)) {\n    const [y, m, d] = value.split(\"-\").map(Number)\n    return new Date(y!, m! - 1, d).toLocaleDateString(\"en-US\", {\n      month: \"short\",\n      day: \"numeric\",\n    })\n  }\n  return String(value)\n}\n\n/** Area shape that masks the fill to a dot grid while keeping the stroke solid. */\nfunction DotGridAreaShape({\n  maskId,\n  ...props\n}: AreaRevealShapeProps & { maskId: string }) {\n  return (\n    <>\n      <g mask={`url(#${maskId})`}>\n        <AreaRevealShape {...props} stroke=\"none\" />\n      </g>\n      <AreaRevealShape {...props} id={undefined} fill=\"none\" />\n    </>\n  )\n}\n\n/** Recharts clamps corner radii to half the bar, so an oversized radius draws a pill. */\nconst PILL = 9999\n\ntype Radius = number | [number, number, number, number]\n\n/** Round only the outer corners of a stack; mixed-sign stacks stay square. */\nfunction barCorners(\n  radius: number | \"full\",\n  stacked: boolean,\n  hasNegative: boolean,\n  horizontal: boolean,\n  index: number,\n  count: number\n): Radius {\n  const r = radius === \"full\" ? PILL : radius\n  if (!stacked || count === 1) return r\n  if (hasNegative) return 0\n  // Corners run [top-left, top-right, bottom-right, bottom-left].\n  if (index === count - 1) return horizontal ? [0, r, r, 0] : [r, r, 0, 0]\n  if (index === 0) return horizontal ? [r, 0, 0, r] : [0, 0, r, r]\n  return 0\n}\n\nfunction TrendChart({\n  data,\n  xKey,\n  series,\n  type = \"area\",\n  fill = \"gradient\",\n  layout = \"vertical\",\n  barRadius = 4,\n  barSize,\n  stacked = false,\n  showLegend = false,\n  legendPosition = \"bottom\",\n  legendAlign = \"center\",\n  showGrid = true,\n  showYAxis = false,\n  yDomain,\n  xFormatter = defaultXFormatter,\n  yFormatter,\n  animate = true,\n  animationDuration = 500,\n  className,\n  ...props\n}: TrendChartProps) {\n  const id = React.useId()\n  const reducedMotion = React.useSyncExternalStore(\n    subscribeToReducedMotion,\n    getReducedMotionSnapshot,\n    getReducedMotionServerSnapshot\n  )\n  const animationProps = {\n    isAnimationActive: animate && !reducedMotion,\n    animationDuration,\n    animationEasing: \"ease-out\" as const,\n  }\n  const dots = type === \"area\" && fill === \"dots\"\n  const horizontal = type === \"bar\" && layout === \"horizontal\"\n  const hasNegative = React.useMemo(\n    () =>\n      data.some((row) =>\n        series.some((s) => {\n          const v = row[s.key]\n          return typeof v === \"number\" && v < 0\n        })\n      ),\n    [data, series]\n  )\n  const config = Object.fromEntries(\n    series.map((s, index) => [\n      s.key,\n      { label: s.label, color: s.color ?? `var(--chart-${(index % 5) + 1})` },\n    ])\n  ) satisfies ChartConfig\n\n  const axes = (\n    <>\n      {showGrid ? (\n        <CartesianGrid vertical={horizontal} horizontal={!horizontal} />\n      ) : null}\n      {horizontal ? (\n        <>\n          <XAxis\n            type=\"number\"\n            hide={!showYAxis}\n            tickLine={false}\n            axisLine={false}\n            tickMargin={8}\n            domain={yDomain}\n            tickFormatter={yFormatter}\n          />\n          <YAxis\n            type=\"category\"\n            dataKey={xKey}\n            tickLine={false}\n            axisLine={false}\n            tickMargin={8}\n            width=\"auto\"\n            tickFormatter={xFormatter}\n          />\n        </>\n      ) : (\n        <>\n          <XAxis\n            dataKey={xKey}\n            tickLine={false}\n            axisLine={false}\n            tickMargin={8}\n            minTickGap={32}\n            tickFormatter={xFormatter}\n          />\n          {showYAxis ? (\n            <YAxis\n              tickLine={false}\n              axisLine={false}\n              tickMargin={8}\n              width=\"auto\"\n              domain={yDomain}\n              tickFormatter={yFormatter}\n            />\n          ) : yDomain ? (\n            <YAxis hide domain={yDomain} />\n          ) : null}\n        </>\n      )}\n      {hasNegative ? (\n        <ReferenceLine\n          {...(horizontal ? { x: 0 } : { y: 0 })}\n          stroke=\"var(--border)\"\n          strokeWidth={1}\n        />\n      ) : null}\n      <ChartTooltip\n        cursor={false}\n        content={\n          <ChartTooltipContent\n            indicator={type === \"bar\" ? \"dashed\" : \"dot\"}\n            labelFormatter={(value) => xFormatter(value)}\n            formatter={\n              yFormatter\n                ? (value, name) => (\n                    <div className=\"flex w-full items-center justify-between gap-4\">\n                      <span className=\"text-muted-foreground\">\n                        {config[name as string]?.label ?? name}\n                      </span>\n                      <span className=\"font-mono font-medium tabular-nums\">\n                        {yFormatter(Number(value))}\n                      </span>\n                    </div>\n                  )\n                : undefined\n            }\n          />\n        }\n      />\n      {showLegend ? (\n        <ChartLegend\n          verticalAlign={legendPosition}\n          align={legendAlign}\n          content={\n            <ChartLegendContent\n              className={\n                legendAlign === \"right\"\n                  ? \"justify-end\"\n                  : legendAlign === \"left\"\n                    ? \"justify-start\"\n                    : undefined\n              }\n            />\n          }\n        />\n      ) : null}\n    </>\n  )\n\n  const margin = {\n    top: 8,\n    right: 8,\n    bottom: 0,\n    left: showYAxis || horizontal ? 0 : 8,\n  }\n\n  return (\n    <ChartContainer\n      data-slot=\"trend-chart\"\n      config={config}\n      className={cn(\"aspect-auto h-64 w-full\", className)}\n      {...props}\n    >\n      {type === \"bar\" ? (\n        <BarChart\n          data={data}\n          layout={horizontal ? \"vertical\" : \"horizontal\"}\n          margin={margin}\n          stackOffset={stacked ? \"sign\" : \"none\"}\n        >\n          {axes}\n          {series.map((s, index) => (\n            <Bar\n              key={s.key}\n              {...animationProps}\n              dataKey={s.key}\n              fill={`var(--color-${s.key})`}\n              barSize={barSize}\n              radius={barCorners(\n                barRadius,\n                stacked,\n                hasNegative,\n                horizontal,\n                index,\n                series.length\n              )}\n              stackId={stacked ? \"stack\" : undefined}\n            />\n          ))}\n        </BarChart>\n      ) : type === \"line\" ? (\n        <LineChart data={data} margin={margin}>\n          {axes}\n          {series.map((s) => (\n            <Line\n              key={s.key}\n              {...animationProps}\n              type=\"monotone\"\n              dataKey={s.key}\n              stroke={`var(--color-${s.key})`}\n              strokeWidth={2}\n              dot={false}\n            />\n          ))}\n        </LineChart>\n      ) : (\n        <AreaChart\n          data={data}\n          margin={margin}\n          stackOffset={stacked ? \"sign\" : \"none\"}\n        >\n          <defs>\n            {series.map((s) => (\n              <linearGradient\n                key={s.key}\n                id={`${id}-${s.key}`}\n                x1=\"0\"\n                y1=\"0\"\n                x2=\"0\"\n                y2=\"1\"\n              >\n                <stop\n                  offset=\"5%\"\n                  stopColor={`var(--color-${s.key})`}\n                  stopOpacity={dots ? 0.08 : 0.6}\n                />\n                <stop\n                  offset=\"95%\"\n                  stopColor={`var(--color-${s.key})`}\n                  stopOpacity={dots ? 0.9 : 0.05}\n                />\n              </linearGradient>\n            ))}\n            {dots ? (\n              <>\n                <pattern\n                  id={`${id}-dots`}\n                  patternUnits=\"userSpaceOnUse\"\n                  width={6}\n                  height={6}\n                >\n                  <circle cx={3} cy={3} r={1.25} fill=\"#fff\" />\n                </pattern>\n                <mask\n                  id={`${id}-mask`}\n                  maskUnits=\"userSpaceOnUse\"\n                  x={0}\n                  y={0}\n                  width=\"100%\"\n                  height=\"100%\"\n                >\n                  <rect width=\"100%\" height=\"100%\" fill={`url(#${id}-dots)`} />\n                </mask>\n              </>\n            ) : null}\n          </defs>\n          {axes}\n          {series.map((s) => (\n            <Area\n              key={s.key}\n              {...animationProps}\n              type=\"monotone\"\n              dataKey={s.key}\n              stroke={`var(--color-${s.key})`}\n              fill={`url(#${id}-${s.key})`}\n              fillOpacity={dots ? 1 : undefined}\n              strokeWidth={2}\n              stackId={stacked ? \"stack\" : undefined}\n              shape={\n                dots\n                  ? (shapeProps: AreaRevealShapeProps) => (\n                      <DotGridAreaShape {...shapeProps} maskId={`${id}-mask`} />\n                    )\n                  : undefined\n              }\n            />\n          ))}\n        </AreaChart>\n      )}\n    </ChartContainer>\n  )\n}\n\nexport { TrendChart }\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}