{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "analytics-chart",
  "type": "registry:ui",
  "description": "Vercel-style gradient line chart with floating tooltip cards",
  "files": [
    {
      "path": "registry/default/ui/analytics-chart.tsx",
      "content": "\"use client\"\n\nimport { useCallback, useEffect, useId, useMemo, useRef, useState } from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* ═══════════════════════════════════════════════════════════════════════\n *  AnalyticsLineChart\n *  Vercel-style gradient line chart with floating tooltip cards.\n *\n *  Props:\n *   data            — { date: string, label: string, value: number }[]\n *   gradientStops   — { offset: string, color: string }[]\n *   dotColors       — string[] (per-index overrides)\n *   gridLines       — number  (default 5)\n *   height          — number  (viewBox height, default 280)\n *   pinnedIndices   — number[] (always-visible tooltip indices)\n *   valueLabel      — string  (unit shown in tooltip, default \"clicks\")\n * ═══════════════════════════════════════════════════════════════════════ */\n\nexport interface AnalyticsDataPoint {\n  date: string\n  label: string\n  value: number\n}\n\nexport interface AnalyticsGradientStop {\n  offset: string\n  color: string\n}\n\nexport const DEFAULT_DATA: AnalyticsDataPoint[] = [\n  { date: \"2023-09-10\", label: \"buy-it-now\", value: 8420 },\n  { date: \"2023-10-15\", label: \"buy-it-now\", value: 12_891 },\n  { date: \"2023-12-10\", label: \"buy-it-now\", value: 21_291 },\n  { date: \"2024-01-20\", label: \"buy-it-now\", value: 28_530 },\n  { date: \"2024-02-15\", label: \"buy-it-now\", value: 30_102 },\n  { date: \"2024-03-10\", label: \"buy-it-now\", value: 33_847 },\n  { date: \"2024-04-10\", label: \"buy-it-now\", value: 40_171 },\n]\n\nexport const DEFAULT_GRADIENT: AnalyticsGradientStop[] = [\n  { offset: \"0%\", color: \"#FF975C\" },\n  { offset: \"33%\", color: \"#E9EB4E\" },\n  { offset: \"66%\", color: \"#9AE13E\" },\n  { offset: \"100%\", color: \"#35C442\" },\n]\n\nexport const PALETTE = [\n  \"#FF975C\",\n  \"#EDBE4D\",\n  \"#CBE749\",\n  \"#A8E040\",\n  \"#7DD83C\",\n  \"#5CCC41\",\n  \"#35C442\",\n]\n\nexport function dotColor(i: number, n: number, overrides?: string[]) {\n  if (overrides?.[i]) {\n    return overrides[i]\n  }\n  const t = n <= 1 ? 1 : i / (n - 1)\n  return PALETTE[\n    Math.min(Math.round(t * (PALETTE.length - 1)), PALETTE.length - 1)\n  ]\n}\n\nexport function fmtDate(s: string) {\n  return new Date(`${s}T00:00:00`).toLocaleDateString(\"en-US\", {\n    weekday: \"short\",\n    day: \"numeric\",\n    month: \"short\",\n    year: \"numeric\",\n  })\n}\n\nexport function fmtNum(n: number) {\n  return n.toLocaleString(\"en-US\")\n}\n\n/* ── Easing ── */\nexport const EASE_OUT = \"cubic-bezier(0.23,1,0.32,1)\"\n\n/* ── Tooltip ────────────────────────────────────────────────────────── */\nexport interface AnalyticsTooltipPoint extends AnalyticsDataPoint {\n  x: number\n  y: number\n  formattedDate: string\n  color: string\n}\n\nexport interface AnalyticsTooltipProps {\n  point: AnalyticsTooltipPoint\n  color: string\n  px: number\n  py: number\n  side: \"top\" | \"bottom\"\n  valueLabel: string\n  show: boolean\n  /** When true, stacks above pinned/static tooltip cards */\n  isHovered?: boolean\n}\n\nexport function AnalyticsTooltip({\n  point,\n  color,\n  px,\n  py,\n  side,\n  valueLabel,\n  show,\n  isHovered = false,\n}: AnalyticsTooltipProps) {\n  const isTop = side === \"top\" // card sits ABOVE the point\n  const circR = 11 // connector circle outer radius\n  const gap = 4 // space between circle edge and card\n\n  // Anchor wrapper is centered exactly on the data point.\n  // Everything else is positioned relative to this anchor.\n  return (\n    <div\n      data-slot=\"analytics-tooltip\"\n      style={{\n        position: \"absolute\",\n        left: px,\n        top: py,\n        // Center the anchor on the data point\n        transform: \"translate(-50%, -50%)\",\n        pointerEvents: \"none\",\n        opacity: show ? 1 : 0,\n        filter: show ? \"blur(0px)\" : \"blur(4px)\",\n        transition: `opacity 220ms ${EASE_OUT}, filter 220ms ${EASE_OUT}`,\n        zIndex: isHovered ? 50 : 10,\n      }}\n    >\n      {/* Connector circle — sits right on the data point (center of wrapper) */}\n      <svg\n        aria-hidden=\"true\"\n        focusable=\"false\"\n        height={circR * 2}\n        style={{ display: \"block\" }}\n        width={circR * 2}\n      >\n        <circle\n          cx={circR}\n          cy={circR}\n          fill=\"var(--card)\"\n          r={circR - 0.5}\n          stroke=\"var(--border)\"\n          strokeWidth=\"1\"\n        />\n        <circle cx={circR} cy={circR} fill={color} r={4.5} />\n      </svg>\n\n      {/* Card — positioned absolutely above or below the circle */}\n      <div\n        data-slot=\"analytics-tooltip-card\"\n        style={{\n          position: \"absolute\",\n          whiteSpace: \"nowrap\",\n          // Horizontal: card's near edge aligns roughly with the circle\n          ...(isTop\n            ? { bottom: circR * 2 + gap, right: -(circR + 2) }\n            : { top: circR * 2 + gap, left: -(circR + 2) }),\n          background: \"var(--card)\",\n          borderRadius: 10,\n          padding: \"9px 13px\",\n          border: \"1px solid var(--border)\",\n          boxShadow: [\n            \"0 1px 2px -1px rgba(0,0,0,0.07)\",\n            \"0 3px 8px 0 rgba(0,0,0,0.04)\",\n            \"0 8px 20px -4px rgba(0,0,0,0.06)\",\n          ].join(\",\"),\n        }}\n      >\n        <div\n          data-slot=\"analytics-tooltip-date\"\n          style={{\n            fontSize: 11,\n            fontWeight: 500,\n            color: \"var(--muted-foreground)\",\n            letterSpacing: \"0.005em\",\n            lineHeight: 1,\n            fontVariantNumeric: \"tabular-nums\",\n          }}\n        >\n          {point.formattedDate}\n        </div>\n        <div\n          data-slot=\"analytics-tooltip-value\"\n          style={{\n            display: \"flex\",\n            alignItems: \"center\",\n            gap: 7,\n            marginTop: 5,\n          }}\n        >\n          <span\n            style={{\n              fontSize: 12.5,\n              fontWeight: 600,\n              color: \"var(--foreground)\",\n              background: `color-mix(in srgb, ${color} 14%, transparent)`,\n              padding: \"1.5px 6px\",\n              borderRadius: 4,\n              lineHeight: \"19px\",\n            }}\n          >\n            {point.label}\n          </span>\n          <span\n            style={{\n              fontSize: 13,\n              fontWeight: 500,\n              color: \"var(--muted-foreground)\",\n              fontVariantNumeric: \"tabular-nums\",\n              lineHeight: \"19px\",\n            }}\n          >\n            {fmtNum(point.value)} {valueLabel}\n          </span>\n        </div>\n      </div>\n    </div>\n  )\n}\n\n/* ── Chart ───────────────────────────────────────────────────────────── */\nexport interface AnalyticsLineChartProps {\n  data?: AnalyticsDataPoint[]\n  gradientStops?: AnalyticsGradientStop[]\n  dotColors?: string[]\n  gridLines?: number\n  height?: number\n  pinnedIndices?: number[]\n  valueLabel?: string\n  className?: string\n}\n\nexport function AnalyticsLineChart({\n  data = DEFAULT_DATA,\n  gradientStops = DEFAULT_GRADIENT,\n  dotColors,\n  gridLines = 5,\n  height = 280,\n  pinnedIndices,\n  valueLabel = \"clicks\",\n  className,\n}: AnalyticsLineChartProps) {\n  const wrapRef = useRef<HTMLDivElement | null>(null)\n  const svgRef = useRef<SVGSVGElement | null>(null)\n  const [hovered, setHovered] = useState<number | null>(null)\n  const [mounted, setMounted] = useState(false)\n  const [wrapRect, setWrapRect] = useState({ w: 600, h: 280 })\n\n  const VB_W = 600\n  const VB_H = height\n  const pad = useMemo(() => ({ t: 48, r: 48, b: 36, l: 48 }), [])\n  const iW = VB_W - pad.l - pad.r\n  const iH = VB_H - pad.t - pad.b\n\n  useEffect(() => {\n    const id = requestAnimationFrame(() => setMounted(true))\n    return () => {\n      cancelAnimationFrame(id)\n      setMounted(false)\n    }\n  }, [])\n\n  useEffect(() => {\n    const el = wrapRef.current\n    if (!el) {\n      return\n    }\n    const ro = new ResizeObserver((entries) => {\n      const r = entries[0].contentRect\n      setWrapRect({ w: r.width, h: r.height })\n    })\n    ro.observe(el)\n    return () => ro.disconnect()\n  }, [])\n\n  const points = useMemo(() => {\n    const vs = data.map((d) => d.value)\n    const mn = Math.min(...vs),\n      mx = Math.max(...vs),\n      rng = mx - mn || 1\n    return data.map((d, i) => ({\n      ...d,\n      x: pad.l + (i / Math.max(data.length - 1, 1)) * iW,\n      y: pad.t + iH - ((d.value - mn) / rng) * iH,\n      formattedDate: fmtDate(d.date),\n      color: dotColor(i, data.length, dotColors),\n    }))\n  }, [data, iW, iH, pad, dotColors])\n\n  const pathD = useMemo(\n    () =>\n      points\n        .map((p, i) => `${i ? \"L\" : \"M\"}${p.x.toFixed(1)},${p.y.toFixed(1)}`)\n        .join(\" \"),\n    [points]\n  )\n\n  const pathLen = useMemo(() => {\n    let l = 0\n    for (let i = 1; i < points.length; i++) {\n      const dx = points[i].x - points[i - 1].x,\n        dy = points[i].y - points[i - 1].y\n      l += Math.sqrt(dx * dx + dy * dy)\n    }\n    return Math.ceil(l)\n  }, [points])\n\n  const pinned = useMemo(() => {\n    if (pinnedIndices) {\n      return pinnedIndices\n    }\n    if (points.length >= 3) {\n      return [2, points.length - 1]\n    }\n    return points.length === 2 ? [0, 1] : [0]\n  }, [pinnedIndices, points.length])\n\n  const onMove = useCallback(\n    (e: React.MouseEvent<SVGSVGElement>) => {\n      const svg = svgRef.current\n      if (!svg) {\n        return\n      }\n      const r = svg.getBoundingClientRect()\n      const mx = ((e.clientX - r.left) / r.width) * VB_W\n      let ci = 0,\n        md = Number.POSITIVE_INFINITY\n      points.forEach((p, i) => {\n        const d = Math.abs(p.x - mx)\n        if (d < md) {\n          md = d\n          ci = i\n        }\n      })\n      setHovered(md < 48 ? ci : null)\n    },\n    [points]\n  )\n\n  const active = useMemo(() => {\n    const s = new Set(pinned)\n    if (hovered !== null) {\n      s.add(hovered)\n    }\n    return s\n  }, [pinned, hovered])\n\n  const sx = wrapRect.w / VB_W\n\n  const gradId = `g-${useId().replaceAll(\":\", \"\")}`\n\n  return (\n    <div\n      className={cn(className)}\n      data-slot=\"analytics-line-chart\"\n      ref={wrapRef}\n      style={{\n        position: \"relative\",\n        maxWidth: VB_W,\n        userSelect: \"none\",\n        WebkitFontSmoothing: \"antialiased\",\n      }}\n    >\n      <svg\n        aria-label=\"Analytics line chart\"\n        data-slot=\"analytics-line-chart-svg\"\n        onMouseLeave={() => setHovered(null)}\n        onMouseMove={onMove}\n        ref={svgRef}\n        role=\"img\"\n        style={{\n          width: \"100%\",\n          height: \"auto\",\n          display: \"block\",\n          overflow: \"visible\",\n        }}\n        viewBox={`0 0 ${VB_W} ${VB_H}`}\n      >\n        <defs>\n          <linearGradient id={gradId} x1=\"0%\" x2=\"100%\" y1=\"0%\" y2=\"0%\">\n            {gradientStops.map((s) => (\n              <stop\n                key={`${s.offset}-${s.color}`}\n                offset={s.offset}\n                stopColor={s.color}\n              />\n            ))}\n          </linearGradient>\n        </defs>\n\n        {/* Grid */}\n        {Array.from({ length: gridLines }).map((_, i) => {\n          const y = pad.t + (i / (gridLines - 1)) * iH\n          return (\n            <line\n              key={`grid-${y.toFixed(2)}`}\n              stroke=\"var(--border)\"\n              strokeWidth=\"1\"\n              style={{\n                opacity: mounted ? 1 : 0,\n                transition: `opacity 600ms ease ${80 + i * 50}ms`,\n              }}\n              x1={pad.l}\n              x2={pad.l + iW}\n              y1={y}\n              y2={y}\n            />\n          )\n        })}\n\n        {/* Line — stroke-dashoffset draw */}\n        <path\n          d={pathD}\n          fill=\"none\"\n          stroke={`url(#${gradId})`}\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          strokeWidth=\"2.5\"\n          style={{\n            strokeDasharray: pathLen,\n            strokeDashoffset: mounted ? 0 : pathLen,\n            transition: `stroke-dashoffset 1000ms ${EASE_OUT}`,\n          }}\n        />\n\n        {/* Dots — hidden when tooltip is active (connector circle takes over) */}\n        {points.map((p, i) => {\n          const isActive = active.has(i)\n          const enterDelay = 700 + i * 80\n          return (\n            <g key={`${p.date}-${p.label}-${p.value}`}>\n              {/* Main dot — fade out when tooltip connector is showing */}\n              <circle\n                cx={p.x}\n                cy={p.y}\n                fill={p.color}\n                r=\"4\"\n                stroke=\"var(--card)\"\n                strokeWidth=\"2.5\"\n                style={{\n                  opacity: mounted ? (isActive ? 0 : 1) : 0,\n                  transition: [\n                    `opacity ${isActive ? \"120ms\" : \"350ms\"} ease ${mounted ? \"0ms\" : `${enterDelay}ms`}`,\n                  ].join(\",\"),\n                }}\n              />\n              {/* Hit target */}\n              <circle\n                cx={p.x}\n                cy={p.y}\n                fill=\"transparent\"\n                r=\"22\"\n                style={{ cursor: \"pointer\" }}\n              />\n            </g>\n          )\n        })}\n      </svg>\n\n      {/* Tooltip layer */}\n      {points.map((p, i) => (\n        <AnalyticsTooltip\n          color={p.color}\n          isHovered={hovered === i}\n          key={`tooltip-${p.date}-${p.label}-${p.value}`}\n          point={p}\n          px={p.x * sx}\n          py={p.y * sx}\n          show={active.has(i) && mounted}\n          side={p.y < pad.t + iH * 0.45 ? \"bottom\" : \"top\"}\n          valueLabel={valueLabel}\n        />\n      ))}\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ]
}