{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "metric-card",
  "title": "MetricCard",
  "description": "Analytics card with a formatted value, signed delta, and interactive Visx sparkline.",
  "dependencies": [
    "recharts",
    "@hugeicons/core-free-icons",
    "@hugeicons/react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.neon.com/r/card.json",
    "https://ui.neon.com/r/badge.json",
    "https://ui.neon.com/r/skeleton.json",
    "https://ui.neon.com/r/neon-loader.json",
    "https://ui.neon.com/r/neon-tokens.json",
    "https://ui.neon.com/r/chart.json"
  ],
  "files": [
    {
      "path": "src/components/metric-card/metric-card.tsx",
      "content": "\"use client\";\n\nimport {\n  Alert02Icon,\n  MinusSignIcon,\n  TradeDownIcon,\n  TradeUpIcon,\n} from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport { useId } from \"react\";\nimport type { ComponentProps, ReactNode } from \"react\";\nimport { Area, AreaChart, XAxis, YAxis } from \"recharts\";\n\nimport { NeonLoader } from \"@/components/neon-loader/neon-loader\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport type { ChartConfig } from \"@/components/ui/chart\";\nimport {\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent,\n} from \"@/components/ui/chart\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { cn } from \"@/lib/utils\";\n\nexport type MetricFormat =\n  | \"number\"\n  | \"bytes\"\n  | \"percent\"\n  | \"currency\"\n  | \"duration\";\n\nexport interface MetricTrendPoint {\n  label: string;\n  value: number;\n}\n\nconst NUMBER_FORMAT = new Intl.NumberFormat(\"en-US\");\nconst CURRENCY_FORMAT = new Intl.NumberFormat(\"en-US\", {\n  currency: \"USD\",\n  style: \"currency\",\n});\nconst BYTE_UNITS = [\"B\", \"KB\", \"MB\", \"GB\", \"TB\", \"PB\"] as const;\nconst BYTE_STEP = 1024;\nconst SECONDS_PER_MINUTE = 60;\nconst SECONDS_PER_HOUR = 3600;\n\nconst formatBytes = (bytes: number) => {\n  if (bytes === 0) {\n    return \"0 B\";\n  }\n\n  const exponent = Math.min(\n    Math.floor(Math.log(Math.abs(bytes)) / Math.log(BYTE_STEP)),\n    BYTE_UNITS.length - 1\n  );\n  const value = bytes / BYTE_STEP ** exponent;\n\n  return `${value.toFixed(value >= 100 || exponent === 0 ? 0 : 1)} ${BYTE_UNITS[exponent]}`;\n};\n\nconst formatDuration = (seconds: number) => {\n  if (seconds < SECONDS_PER_MINUTE) {\n    return `${Math.round(seconds)}s`;\n  }\n  if (seconds < SECONDS_PER_HOUR) {\n    return `${Math.floor(seconds / SECONDS_PER_MINUTE)}m ${Math.round(seconds % SECONDS_PER_MINUTE)}s`;\n  }\n\n  return `${Math.floor(seconds / SECONDS_PER_HOUR)}h ${Math.floor((seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE)}m`;\n};\n\nconst formatValue = (value: number | string, format: MetricFormat) => {\n  if (typeof value === \"string\") {\n    return value;\n  }\n\n  switch (format) {\n    case \"bytes\": {\n      return formatBytes(value);\n    }\n    case \"percent\": {\n      return `${NUMBER_FORMAT.format(value)}%`;\n    }\n    case \"currency\": {\n      return CURRENCY_FORMAT.format(value);\n    }\n    case \"duration\": {\n      return formatDuration(value);\n    }\n    default: {\n      return NUMBER_FORMAT.format(value);\n    }\n  }\n};\n\nconst deltaDirection = (delta: number | undefined): \"up\" | \"down\" | \"flat\" => {\n  if (delta === undefined || delta === 0) {\n    return \"flat\";\n  }\n\n  return delta > 0 ? \"up\" : \"down\";\n};\n\nconst DeltaBadge = ({ delta }: { delta: number }) => {\n  const direction = deltaDirection(delta);\n\n  return (\n    <Badge\n      className={cn(\n        \"h-5 border-0 px-1.5 py-0 text-[11px] tabular-nums shadow-none\",\n        direction === \"up\" && \"bg-primary/10 text-primary\",\n        direction === \"down\" && \"bg-destructive/10 text-destructive\",\n        direction === \"flat\" && \"bg-muted text-muted-foreground\"\n      )}\n      variant=\"secondary\"\n    >\n      {direction === \"up\" ? (\n        <HugeiconsIcon icon={TradeUpIcon} strokeWidth={2} />\n      ) : null}\n      {direction === \"down\" ? (\n        <HugeiconsIcon icon={TradeDownIcon} strokeWidth={2} />\n      ) : null}\n      {direction === \"flat\" ? (\n        <HugeiconsIcon icon={MinusSignIcon} strokeWidth={2} />\n      ) : null}\n      {direction === \"up\" ? \"+\" : \"\"}\n      {NUMBER_FORMAT.format(Math.abs(delta))}%\n    </Badge>\n  );\n};\n\nconst CHART_HEIGHT = 56;\n\ninterface TrendPoint extends MetricTrendPoint {\n  index: number;\n}\n\nconst normalizeTrend = (trend: (number | MetricTrendPoint)[]): TrendPoint[] =>\n  trend.map((point, index) => ({\n    index,\n    label: typeof point === \"number\" ? `Point ${index + 1}` : point.label,\n    value: typeof point === \"number\" ? point : point.value,\n  }));\n\n/** Row renderer for the sparkline tooltip: the metric's own name and value. */\nconst trendTooltipFormatter = (\n  label: string,\n  format: MetricFormat,\n  unitText: string\n) =>\n  function TrendTooltipRow(value: unknown) {\n    return (\n      <div className=\"flex flex-1 items-center justify-between gap-3 leading-none\">\n        <span className=\"text-muted-foreground\">{label}</span>\n        <span className=\"font-medium font-mono text-foreground tabular-nums\">\n          {formatValue(Number(value), format)}\n          {unitText}\n        </span>\n      </div>\n    );\n  };\n\nconst TrendChart = ({\n  direction,\n  format,\n  label,\n  trend,\n  unit,\n}: {\n  direction: \"up\" | \"down\" | \"flat\";\n  format: MetricFormat;\n  label: string;\n  trend: (number | MetricTrendPoint)[];\n  unit?: ReactNode;\n}) => {\n  const chartId = useId().replaceAll(\":\", \"\");\n  const gradientId = `metric-trend-gradient-${chartId}`;\n  const stripeId = `metric-trend-stripes-${chartId}`;\n  const data = normalizeTrend(trend);\n  const values = data.map((point) => point.value);\n  const minimum = Math.min(...values);\n  const maximum = Math.max(...values);\n  const domainPadding = (maximum - minimum || 1) * 0.12;\n  const color = direction === \"down\" ? \"var(--destructive)\" : \"var(--primary)\";\n  const unitText = typeof unit === \"string\" ? ` ${unit}` : \"\";\n  const firstLabel = data[0]?.label ?? \"\";\n  const middleLabel = data[Math.round((data.length - 1) / 2)]?.label ?? \"\";\n  const lastLabel = data.at(-1)?.label ?? \"\";\n\n  const config: ChartConfig = {\n    value: { color, label },\n  };\n\n  return (\n    <div className=\"relative\">\n      <ChartContainer className=\"aspect-auto h-14 w-full\" config={config}>\n        <AreaChart\n          accessibilityLayer\n          data={data}\n          margin={{ bottom: 2, left: 0, right: 0, top: 2 }}\n        >\n          <defs>\n            <linearGradient id={gradientId} x1=\"0\" x2=\"0\" y1=\"0\" y2=\"1\">\n              <stop offset=\"0%\" stopColor={color} stopOpacity={0.18} />\n              <stop offset=\"100%\" stopColor={color} stopOpacity={0} />\n            </linearGradient>\n            {/* House texture: a hairline rule every 8px under the fill. */}\n            <pattern\n              height={CHART_HEIGHT}\n              id={stripeId}\n              patternUnits=\"userSpaceOnUse\"\n              width=\"8\"\n            >\n              <line\n                stroke={color}\n                strokeOpacity={0.16}\n                strokeWidth={0.75}\n                x1=\"0.5\"\n                x2=\"0.5\"\n                y1=\"0\"\n                y2={CHART_HEIGHT}\n              />\n            </pattern>\n          </defs>\n\n          <XAxis dataKey=\"label\" hide />\n          <YAxis\n            domain={[minimum - domainPadding, maximum + domainPadding]}\n            hide\n          />\n          <ChartTooltip\n            content={\n              <ChartTooltipContent\n                formatter={trendTooltipFormatter(label, format, unitText)}\n                hideIndicator\n                labelKey=\"label\"\n              />\n            }\n            cursor={{ stroke: \"var(--border)\", strokeDasharray: \"2 3\" }}\n          />\n\n          <Area\n            dataKey=\"value\"\n            fill={`url(#${gradientId})`}\n            stroke=\"none\"\n            type=\"monotone\"\n          />\n          <Area\n            dataKey=\"value\"\n            fill={`url(#${stripeId})`}\n            stroke={color}\n            strokeWidth={1.75}\n            type=\"monotone\"\n          />\n        </AreaChart>\n      </ChartContainer>\n\n      <div\n        aria-hidden=\"true\"\n        className=\"mt-1 grid grid-cols-3 px-[3px] text-[9px] text-muted-foreground/70 leading-none tabular-nums\"\n      >\n        <span>{firstLabel}</span>\n        <span className=\"text-center\">{middleLabel}</span>\n        <span className=\"text-right\">{lastLabel}</span>\n      </div>\n    </div>\n  );\n};\n\nexport type MetricCardProps = Omit<ComponentProps<typeof Card>, \"children\"> & {\n  label: string;\n  value: number | string;\n  /** Signed percentage change versus the previous period. */\n  delta?: number;\n  /** Human-readable context for the delta (for example, \"vs yesterday\"). */\n  comparisonLabel?: string;\n  /** Series driving the trend chart; needs at least two points to render. */\n  trend?: (number | MetricTrendPoint)[];\n  format?: MetricFormat;\n  /** Unit suffix rendered after the value (for example, \"hrs\"). */\n  unit?: ReactNode;\n  isLoading?: boolean;\n  error?: Error | string | null;\n};\n\nconst metricCardClassName =\n  \"min-h-[168px] gap-0 overflow-hidden rounded-lg border border-border/60 bg-card py-0 shadow-none ring-0 transition-colors hover:border-border\";\n\nexport const MetricCard = ({\n  label,\n  value,\n  delta,\n  comparisonLabel,\n  trend,\n  format = \"number\",\n  unit,\n  isLoading = false,\n  error = null,\n  className,\n  ...props\n}: MetricCardProps) => {\n  if (isLoading) {\n    return (\n      <Card\n        aria-busy=\"true\"\n        aria-label={`Loading ${label}`}\n        className={cn(metricCardClassName, className)}\n        {...props}\n      >\n        <CardHeader className=\"grid grid-cols-[minmax(0,1fr)_auto] items-center gap-3 px-4 pt-4\">\n          <CardTitle\n            className=\"truncate font-mono font-medium text-muted-foreground text-xs\"\n            title={label}\n          >\n            {label}\n          </CardTitle>\n          <NeonLoader\n            className=\"shrink-0\"\n            label=\"Loading metric data\"\n            size={16}\n          />\n        </CardHeader>\n        <CardContent className=\"mt-auto px-4 pt-3 pb-3\">\n          <Skeleton aria-hidden=\"true\" className=\"h-8 w-24\" />\n          <Skeleton\n            aria-hidden=\"true\"\n            className=\"mt-3 h-[72px] w-full bg-muted/60\"\n          />\n        </CardContent>\n      </Card>\n    );\n  }\n\n  if (error) {\n    const message = typeof error === \"string\" ? error : error.message;\n\n    return (\n      <Card\n        className={cn(metricCardClassName, className)}\n        role=\"alert\"\n        {...props}\n      >\n        <CardHeader className=\"px-4 pt-4\">\n          <CardTitle\n            className=\"truncate font-mono font-medium text-muted-foreground text-xs\"\n            title={label}\n          >\n            {label}\n          </CardTitle>\n        </CardHeader>\n        <CardContent className=\"mt-auto px-4 pt-3 pb-3\">\n          <div className=\"rounded-md border border-destructive/20 bg-destructive/[0.045] p-3\">\n            <div className=\"flex items-center gap-2 text-destructive\">\n              <HugeiconsIcon\n                aria-hidden=\"true\"\n                className=\"size-3.5\"\n                icon={Alert02Icon}\n                strokeWidth={2}\n              />\n              <p className=\"font-medium text-xs\">Data unavailable</p>\n            </div>\n            <p className=\"mt-2 text-pretty text-muted-foreground text-xs leading-relaxed\">\n              {message}\n            </p>\n          </div>\n        </CardContent>\n      </Card>\n    );\n  }\n\n  const direction = deltaDirection(delta);\n\n  return (\n    <Card className={cn(metricCardClassName, className)} {...props}>\n      <CardHeader className=\"grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3 px-4 pt-4\">\n        <CardTitle\n          className=\"truncate font-mono font-medium text-muted-foreground text-xs\"\n          title={label}\n        >\n          {label}\n        </CardTitle>\n        {delta === undefined ? null : (\n          <div className=\"flex shrink-0 flex-col items-end gap-1\">\n            <DeltaBadge delta={delta} />\n            {comparisonLabel ? (\n              <span className=\"whitespace-nowrap text-[10px] text-muted-foreground leading-none\">\n                {comparisonLabel}\n              </span>\n            ) : null}\n          </div>\n        )}\n      </CardHeader>\n\n      <CardContent className=\"mt-auto px-4 pt-3 pb-3\">\n        <div className=\"flex items-baseline gap-1.5\">\n          <span className=\"font-semibold text-3xl tracking-tight tabular-nums\">\n            {formatValue(value, format)}\n          </span>\n          {unit ? (\n            <span className=\"text-muted-foreground text-sm\">{unit}</span>\n          ) : null}\n        </div>\n\n        {trend === undefined ? null : (\n          <div\n            className={cn(\n              \"relative -mx-1 mt-3 overflow-hidden bg-gradient-to-b to-transparent px-1 pt-1\",\n              trend.length <= 1 && \"from-muted/20\",\n              trend.length > 1 && direction === \"up\" && \"from-primary/[0.045]\",\n              trend.length > 1 &&\n                direction === \"down\" &&\n                \"from-destructive/[0.07]\",\n              trend.length > 1 && direction === \"flat\" && \"from-muted/20\"\n            )}\n          >\n            {trend.length > 1 ? (\n              <TrendChart\n                direction={direction}\n                format={format}\n                label={label}\n                trend={trend}\n                unit={unit}\n              />\n            ) : (\n              <div className=\"flex h-[72px] items-center justify-center gap-2 text-muted-foreground\">\n                <HugeiconsIcon\n                  aria-hidden=\"true\"\n                  className=\"size-3.5\"\n                  icon={MinusSignIcon}\n                  strokeWidth={2}\n                />\n                <span className=\"text-[11px]\">No trend data</span>\n              </div>\n            )}\n          </div>\n        )}\n      </CardContent>\n    </Card>\n  );\n};\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}