{
  "name": "line-chart-draw",
  "type": "registry:ui",
  "description": "SVG line chart that draws itself on",
  "dependencies": [
    "remotion",
    "@remotion/paths"
  ],
  "registryDependencies": [
    "chart-utils",
    "path-utils",
    "motion-tokens"
  ],
  "files": [
    {
      "path": "registry/bases/default/primitives/line-chart-draw.tsx",
      "type": "registry:ui",
      "content": "import { useId } from \"react\";\nimport { getLength, getPointAtLength } from \"@remotion/paths\";\nimport { interpolate, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport {\n  buildAreaPath,\n  buildLinePath,\n  buildSmoothPath,\n  formatAxisValue,\n  getPlotArea,\n  niceDomain,\n  plotPoints,\n  type ChartPoint,\n} from \"@/remotion/lib/chart-utils\";\nimport { EASING } from \"@/remotion/lib/motion-tokens\";\nimport { getPathDrawStyles } from \"@/remotion/lib/path-utils\";\n\nexport type LineChartDrawProps = {\n  points: ChartPoint[];\n  /** Drawing width. Defaults to the composition width. */\n  width?: number;\n  /** Drawing height. Defaults to 40% of the composition height. */\n  height?: number;\n  color?: string;\n  strokeWidth?: number;\n  /** Cardinal spline through the points, or straight segments. */\n  variant?: \"smooth\" | \"linear\";\n  /** Gridlines and value labels down the left gutter. */\n  showAxis?: boolean;\n  /** Category labels under the plot, read from `point.label`. */\n  showXLabels?: boolean;\n  /** Gradient fill under the line, wiped in with the draw. */\n  showArea?: boolean;\n  /** Dot deposited on each data point as the line passes it. */\n  showDots?: boolean;\n  /** Glowing dot riding the tip of the line while it draws. */\n  showHead?: boolean;\n  /** Value callout pinned to the final point once the draw lands. */\n  showEndLabel?: boolean;\n  /**\n   * Anchor the value axis at zero. Turn it off for sparklines, where the shape\n   * of the recent range matters more than its distance from zero.\n   */\n  includeZero?: boolean;\n  /** Formats axis ticks and the end label. */\n  valueFormatter?: (value: number) => string;\n  /** Text colour for axis and category labels. */\n  labelColor?: string;\n  gridColor?: string;\n  /** Ink behind the dots — match the surface the chart sits on. */\n  surfaceColor?: string;\n  durationInFrames?: number;\n  delayInFrames?: number;\n  /** Frame override — pass the parent frame inside a `<Sequence>`. */\n  frame?: number;\n};\n\nconst clamp01 = (value: number) => Math.min(1, Math.max(0, value));\n\n/**\n * A line chart that draws itself on.\n *\n * The line, its fill, gridlines and labels are all projected from one plot\n * rectangle (see `chart-utils`), so the geometry stays locked together at any\n * size. The draw is a single progress value: the stroke evolves along its own\n * length, the fill is wiped by a clip rect, and each dot lands as the tip\n * passes it — one gesture instead of several animations that merely overlap.\n */\nexport const LineChartDraw: React.FC<LineChartDrawProps> = ({\n  points,\n  width: widthProp,\n  height: heightProp,\n  color = \"#e8b86d\",\n  strokeWidth: strokeWidthProp,\n  variant = \"smooth\",\n  showAxis = true,\n  showXLabels = true,\n  showArea = true,\n  showDots = true,\n  showHead = true,\n  showEndLabel = false,\n  includeZero = true,\n  valueFormatter = formatAxisValue,\n  labelColor = \"rgba(250,250,250,0.52)\",\n  gridColor = \"rgba(250,250,250,0.10)\",\n  surfaceColor = \"#080810\",\n  durationInFrames = 70,\n  delayInFrames = 0,\n  frame: frameOverride,\n}) => {\n  const localFrame = useCurrentFrame();\n  const frame = frameOverride ?? localFrame;\n  const config = useVideoConfig();\n  const width = widthProp ?? config.width;\n  const height = heightProp ?? Math.round(config.height * 0.4);\n\n  // Type scales off the chart's own width, so a chart dropped into a narrow\n  // column keeps the same label-to-plot ratio as a full-bleed one.\n  const unit = width / 960;\n  const scale = (value: number) => Math.max(1, Math.round(value * unit));\n  const labelSize = scale(22);\n  const strokeWidth = strokeWidthProp ?? scale(5);\n\n  const domain = niceDomain(\n    points.map((point) => point.y),\n    { includeZero, tickCount: 4 },\n  );\n  // Called with one argument on purpose — a point-free `.map(valueFormatter)`\n  // would feed the array index into a formatter's second parameter.\n  const axisLabels = domain.ticks.map((tick) => valueFormatter(tick));\n  const longestLabel = axisLabels.reduce(\n    (longest, label) => Math.max(longest, label.length),\n    0,\n  );\n  const gutter = showAxis\n    ? Math.round(longestLabel * labelSize * 0.62) + scale(18)\n    : scale(6);\n\n  const plot = getPlotArea(width, height, {\n    top: scale(showEndLabel ? 34 : 16),\n    right: scale(showEndLabel ? 44 : 12),\n    bottom: showXLabels ? labelSize + scale(20) : scale(10),\n    left: gutter,\n  });\n\n  const plotted = plotPoints(points, plot, domain);\n  const linePath =\n    variant === \"smooth\" ? buildSmoothPath(plotted) : buildLinePath(plotted);\n  const areaPath = buildAreaPath(linePath, plotted, plot.bottom);\n\n  const progress = interpolate(\n    frame,\n    [delayInFrames, delayInFrames + durationInFrames],\n    [0, 1],\n    {\n      extrapolateLeft: \"clamp\",\n      extrapolateRight: \"clamp\",\n      easing: EASING.enter,\n    },\n  );\n\n  // The tip is read off the real path rather than inferred from progress, so\n  // it tracks the curve instead of floating beside it on steep segments.\n  const pathLength = linePath === \"\" ? 0 : getLength(linePath);\n  const fallbackTip = { x: plot.left, y: plot.bottom };\n  const tip =\n    (pathLength > 0\n      ? getPointAtLength(linePath, pathLength * progress)\n      : null) ?? fallbackTip;\n\n  // Two charts on one frame would otherwise share a gradient id and the second\n  // would repaint the first.\n  const uid = useId().replace(/:/g, \"\");\n  const gradientId = `${uid}-fill`;\n  const clipId = `${uid}-clip`;\n\n  const endPoint = plotted[plotted.length - 1];\n  const endLabelOpacity = interpolate(progress, [0.88, 1], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n\n  return (\n    <svg\n      width={width}\n      height={height}\n      viewBox={`0 0 ${width} ${height}`}\n      style={{ overflow: \"visible\" }}\n    >\n      <defs>\n        <linearGradient id={gradientId} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n          <stop offset=\"0%\" stopColor={color} stopOpacity={0.32} />\n          <stop offset=\"100%\" stopColor={color} stopOpacity={0} />\n        </linearGradient>\n        <clipPath id={clipId}>\n          <rect\n            x={plot.left}\n            y={plot.top - strokeWidth}\n            width={plot.width * progress}\n            height={plot.height + strokeWidth * 2}\n          />\n        </clipPath>\n      </defs>\n\n      {showAxis\n        ? domain.ticks.map((tick, index) => {\n            const y =\n              plot.bottom - ((tick - domain.min) / domain.span) * plot.height;\n\n            return (\n              <g key={tick}>\n                <line\n                  x1={plot.left}\n                  y1={y}\n                  x2={plot.right}\n                  y2={y}\n                  stroke={gridColor}\n                  strokeWidth={1}\n                  // The zero line carries the baseline, so it reads heavier.\n                  opacity={index === 0 ? 1.8 : 1}\n                />\n                <text\n                  x={plot.left - scale(12)}\n                  y={y}\n                  fill={labelColor}\n                  fontSize={labelSize}\n                  fontWeight={600}\n                  textAnchor=\"end\"\n                  dominantBaseline=\"central\"\n                  style={{ fontVariantNumeric: \"tabular-nums\" }}\n                >\n                  {axisLabels[index]}\n                </text>\n              </g>\n            );\n          })\n        : null}\n\n      {showArea && areaPath !== \"\" ? (\n        <path\n          d={areaPath}\n          fill={`url(#${gradientId})`}\n          clipPath={`url(#${clipId})`}\n        />\n      ) : null}\n\n      <path\n        d={linePath}\n        fill=\"none\"\n        stroke={color}\n        strokeWidth={strokeWidth}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        style={getPathDrawStyles(progress, linePath)}\n      />\n\n      {showXLabels\n        ? plotted.map((point, index) => (\n            <text\n              key={`${point.x}-label`}\n              x={point.x}\n              y={plot.bottom + scale(14) + labelSize / 2}\n              fill={labelColor}\n              fontSize={labelSize}\n              fontWeight={600}\n              // End labels anchor inward so they never clip the plot edges.\n              textAnchor={\n                index === 0\n                  ? \"start\"\n                  : index === plotted.length - 1\n                    ? \"end\"\n                    : \"middle\"\n              }\n              dominantBaseline=\"central\"\n            >\n              {point.label ?? \"\"}\n            </text>\n          ))\n        : null}\n\n      {showDots\n        ? plotted.map((point) => {\n            // Dots grow in as the tip clears them, over a short window, so the\n            // reveal reads as the line depositing them rather than a blink.\n            const window = Math.max(1, plot.width * 0.04);\n            const reveal = clamp01((tip.x - point.x + window) / window);\n\n            return (\n              <circle\n                key={`${point.x}-dot`}\n                cx={point.x}\n                cy={point.y}\n                r={strokeWidth * 0.95 * reveal}\n                fill={color}\n                stroke={surfaceColor}\n                strokeWidth={strokeWidth * 0.5 * reveal}\n              />\n            );\n          })\n        : null}\n\n      {showHead && progress > 0 && progress < 1 ? (\n        <g>\n          <circle\n            cx={tip.x}\n            cy={tip.y}\n            r={strokeWidth * 2.6}\n            fill={color}\n            opacity={0.18}\n          />\n          <circle cx={tip.x} cy={tip.y} r={strokeWidth * 1.05} fill={color} />\n        </g>\n      ) : null}\n\n      {showEndLabel && endPoint ? (\n        <text\n          x={endPoint.x}\n          y={endPoint.y - scale(22)}\n          fill={color}\n          fontSize={scale(28)}\n          fontWeight={700}\n          textAnchor=\"end\"\n          dominantBaseline=\"central\"\n          opacity={endLabelOpacity}\n          style={{ fontVariantNumeric: \"tabular-nums\" }}\n        >\n          {valueFormatter(endPoint.value)}\n        </text>\n      ) : null}\n    </svg>\n  );\n};\n"
    }
  ],
  "atlas": {
    "lane": "signals",
    "drive": "data",
    "tier": "advanced",
    "tags": [
      "charts"
    ]
  }
}