{
  "name": "metric-ticker",
  "type": "registry:block",
  "description": "KPI cards that count themselves in",
  "dependencies": [
    "remotion",
    "@remotion/paths",
    "@remotion/google-fonts"
  ],
  "registryDependencies": [
    "line-chart-draw",
    "chart-utils",
    "layout",
    "motion-tokens"
  ],
  "files": [
    {
      "path": "registry/bases/default/scenes/metric-ticker/index.tsx",
      "type": "registry:block",
      "content": "import { loadFont } from \"@remotion/google-fonts/Inter\";\nimport { interpolate, spring, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { LineChartDraw } from \"@/remotion/primitives/line-chart-draw\";\nimport { formatCompactNumber, readDelta } from \"@/remotion/lib/chart-utils\";\nimport { getSafeAreaPadding, scaleFont } from \"@/remotion/lib/layout\";\nimport { DURATION, EASING, STAGGER } from \"@/remotion/lib/motion-tokens\";\n\nconst { fontFamily } = loadFont(\"normal\", {\n  weights: [\"400\", \"500\", \"600\", \"700\"],\n  subsets: [\"latin\"],\n});\n\nexport type MetricTickerItem = {\n  label: string;\n  value: number;\n  /** Unit appended to the counted value, e.g. `\"min\"`. */\n  suffix?: string;\n  /** Symbol placed before the value, e.g. `\"$\"`. */\n  prefix?: string;\n  /** Signed change, e.g. `\"+18%\"` — the sign picks the colour and arrow. */\n  delta?: string;\n  /** Recent history, drawn as a sparkline under the value. */\n  trend?: number[];\n  /** Overrides `accentColor` for this card. */\n  color?: string;\n};\n\nexport type MetricTickerProps = {\n  metrics: MetricTickerItem[];\n  title?: string;\n  /** Short label above the title. */\n  eyebrow?: string;\n  /** Formats the counted value. */\n  valueFormatter?: (value: number) => string;\n  /** Cards beyond this count are dropped rather than squeezed. */\n  maxCards?: number;\n  backgroundColor?: string;\n  accentColor?: string;\n};\n\nconst COLORS = {\n  bg: \"#080810\",\n  ink: \"#fafafa\",\n  label: \"rgba(250,250,250,0.58)\",\n  eyebrow: \"rgba(250,250,250,0.44)\",\n  card: \"rgba(250,250,250,0.035)\",\n  border: \"rgba(250,250,250,0.10)\",\n  accent: \"#e8b86d\",\n  up: \"#2dd4bf\",\n  down: \"#f87171\",\n} as const;\n\nconst DELTA_GLYPH = { up: \"↑\", down: \"↓\", flat: \"\" } as const;\n\n/**\n * KPI cards that count themselves in.\n *\n * Cards divide the full content width instead of hugging the left edge, so the\n * row reads as one instrument panel. The counter, the delta chip and the\n * sparkline are all driven off the card's own spring, staggered just enough\n * that the eye lands on each card in reading order.\n */\nexport const MetricTicker: React.FC<MetricTickerProps> = ({\n  metrics,\n  title,\n  eyebrow,\n  valueFormatter = formatCompactNumber,\n  maxCards = 4,\n  backgroundColor = COLORS.bg,\n  accentColor = COLORS.accent,\n}) => {\n  const frame = useCurrentFrame();\n  const { fps, width, height } = useVideoConfig();\n  const safe = getSafeAreaPadding({ width, height });\n  const isPortrait = height > width;\n\n  const cards = metrics.slice(0, maxCards);\n  const columns = isPortrait ? 1 : Math.max(1, cards.length);\n  const gap = scaleFont(20, width);\n  const cardPadding = scaleFont(28, width);\n  const contentWidth = width - safe.paddingLeft - safe.paddingRight;\n  const cardWidth = (contentWidth - gap * (columns - 1)) / columns;\n  // Sparklines are drawn at an explicit width, so the card geometry has to be\n  // known here rather than left to the layout engine.\n  const sparkWidth = Math.max(1, Math.round(cardWidth - cardPadding * 2));\n  const sparkHeight = scaleFont(isPortrait ? 56 : 64, width);\n\n  const headerProgress = interpolate(frame, [0, DURATION.normal], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\n  return (\n    <div\n      style={{\n        width,\n        height,\n        background: backgroundColor,\n        backgroundImage: `radial-gradient(ellipse 70% 50% at 50% 0%, ${accentColor}12, transparent 70%)`,\n        color: COLORS.ink,\n        fontFamily,\n        paddingLeft: safe.paddingLeft,\n        paddingRight: safe.paddingRight,\n        paddingTop: safe.paddingTop,\n        paddingBottom: safe.paddingBottom,\n        display: \"flex\",\n        flexDirection: \"column\",\n        justifyContent: \"center\",\n        gap: scaleFont(44, width),\n      }}\n    >\n      {title || eyebrow ? (\n        <header\n          style={{\n            display: \"grid\",\n            gap: scaleFont(10, width),\n            opacity: headerProgress,\n            transform: `translateY(${(1 - headerProgress) * scaleFont(16, width)}px)`,\n          }}\n        >\n          {eyebrow ? (\n            <span\n              style={{\n                color: COLORS.eyebrow,\n                fontSize: scaleFont(24, width),\n                fontWeight: 600,\n                letterSpacing: \"0.08em\",\n                textTransform: \"uppercase\",\n              }}\n            >\n              {eyebrow}\n            </span>\n          ) : null}\n          {title ? (\n            <h2\n              style={{\n                margin: 0,\n                fontSize: scaleFont(isPortrait ? 72 : 58, width),\n                fontWeight: 700,\n                lineHeight: 1.02,\n                letterSpacing: \"-0.025em\",\n              }}\n            >\n              {title}\n            </h2>\n          ) : null}\n        </header>\n      ) : null}\n\n      <div\n        style={{\n          display: \"grid\",\n          gridTemplateColumns: `repeat(${columns}, 1fr)`,\n          gap,\n        }}\n      >\n        {cards.map((metric, index) => {\n          const delay = STAGGER.normal + index * STAGGER.normal;\n          const enter = spring({\n            frame: frame - delay,\n            fps,\n            config: { damping: 20, stiffness: 120, mass: 0.9 },\n            durationInFrames: DURATION.slow,\n          });\n          const delta = readDelta(metric.delta);\n          const deltaColor =\n            delta.direction === \"down\"\n              ? COLORS.down\n              : delta.direction === \"up\"\n                ? COLORS.up\n                : COLORS.label;\n          const cardColor = metric.color ?? accentColor;\n          const detailOpacity = interpolate(enter, [0.55, 1], [0, 1], {\n            extrapolateLeft: \"clamp\",\n            extrapolateRight: \"clamp\",\n          });\n\n          return (\n            <div\n              key={metric.label}\n              style={{\n                display: \"grid\",\n                gap: scaleFont(14, width),\n                alignContent: \"start\",\n                padding: cardPadding,\n                borderRadius: scaleFont(18, width),\n                background: COLORS.card,\n                border: `1px solid ${COLORS.border}`,\n                opacity: Math.min(1, enter * 1.6),\n                transform: `translateY(${(1 - enter) * scaleFont(22, width)}px)`,\n              }}\n            >\n              <div\n                style={{\n                  color: COLORS.label,\n                  fontSize: scaleFont(24, width),\n                  fontWeight: 600,\n                  lineHeight: 1,\n                }}\n              >\n                {metric.label}\n              </div>\n\n              <div\n                style={{\n                  display: \"flex\",\n                  alignItems: \"baseline\",\n                  gap: scaleFont(12, width),\n                  flexWrap: \"wrap\",\n                }}\n              >\n                <span\n                  style={{\n                    color: cardColor,\n                    fontSize: scaleFont(isPortrait ? 76 : 64, width),\n                    fontWeight: 700,\n                    lineHeight: 1,\n                    letterSpacing: \"-0.03em\",\n                    fontVariantNumeric: \"tabular-nums\",\n                  }}\n                >\n                  {metric.prefix ?? \"\"}\n                  {valueFormatter(metric.value * enter)}\n                  {metric.suffix ?? \"\"}\n                </span>\n                {delta.text ? (\n                  <span\n                    style={{\n                      color: deltaColor,\n                      fontSize: scaleFont(22, width),\n                      fontWeight: 600,\n                      lineHeight: 1,\n                      opacity: detailOpacity,\n                    }}\n                  >\n                    {DELTA_GLYPH[delta.direction]} {delta.text}\n                  </span>\n                ) : null}\n              </div>\n\n              {metric.trend && metric.trend.length > 1 ? (\n                <div style={{ opacity: detailOpacity }}>\n                  <LineChartDraw\n                    points={metric.trend.map((value, pointIndex) => ({\n                      x: pointIndex,\n                      y: value,\n                    }))}\n                    width={sparkWidth}\n                    height={sparkHeight}\n                    // Tinting the sparkline by direction keeps the card from\n                    // reading as one flat accent wash and repeats the delta's\n                    // meaning in the shape below it.\n                    color={delta.text ? deltaColor : cardColor}\n                    strokeWidth={Math.max(2, scaleFont(3, width))}\n                    showAxis={false}\n                    showXLabels={false}\n                    includeZero={false}\n                    showDots={false}\n                    showHead={false}\n                    durationInFrames={DURATION.slow}\n                    delayInFrames={delay + STAGGER.tight}\n                    frame={frame}\n                  />\n                </div>\n              ) : null}\n            </div>\n          );\n        })}\n      </div>\n    </div>\n  );\n};\n"
    }
  ],
  "atlas": {
    "lane": "signals",
    "drive": "data",
    "tier": "advanced",
    "tags": [
      "charts",
      "metrics"
    ]
  }
}