{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pixel-bars",
  "title": "Pixel Bars",
  "description": "LED-style pixel bars climb, the last column lighting orange and blue to +64%.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/pixel-bars/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont } from \"@remotion/google-fonts/SpaceGrotesk\";\nimport { useId } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst { fontFamily: grotesk } = loadFont(\"normal\", {\n  weights: [\"400\", \"500\"],\n  subsets: [\"latin\"],\n});\n\nexport interface PixelBarsProps {\n  values?: number[];\n  label?: string;\n  metric?: string;\n  accent?: string;\n  theme?: \"dark\" | \"light\";\n  speed?: number;\n}\n\nconst DEFAULT_VALUES = [31, 38, 35, 44, 41, 50, 84];\n\nconst EASE_IO = Easing.bezier(0.65, 0, 0.35, 1);\nconst EASE_OUT = Easing.bezier(0.22, 1, 0.36, 1);\nconst POP = Easing.bezier(0.3, 1.5, 0.5, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\n// gradients.sh \"fluted\" palette, run bottom-to-top through the hero column.\nconst FLUTED = [\"#1d2a8f\", \"#2f7bff\", \"#ff7a3d\", \"#ffe2c4\"];\n\nconst THEMES = {\n  dark: {\n    bg: \"#0A0A0B\",\n    fg: \"#F3F2EF\",\n    muted: \"#A9A8AD\",\n    cell: \"#3E3E44\",\n    lead: \"#A3A2A8\",\n    slot: \"rgba(255,255,255,0.05)\",\n    hair: \"rgba(255,255,255,0.2)\",\n    light: \"rgba(255,255,255,0.045)\",\n    top: FLUTED[3],\n    grain: 0.06,\n  },\n  light: {\n    bg: \"#F5F4F0\",\n    fg: \"#121214\",\n    muted: \"#6E6C67\",\n    cell: \"#BDBBB5\",\n    lead: \"#6E6C67\",\n    slot: \"rgba(18,18,20,0.05)\",\n    hair: \"rgba(18,18,20,0.22)\",\n    light: \"rgba(255,255,255,0.75)\",\n    top: \"#ffb27f\",\n    grain: 0.045,\n  },\n} as const;\n\n// Space Grotesk 500 advances (em), tabular digits.\nconst NUM_ADV: Record<string, number> = {\n  \"+\": 0.62,\n  \"-\": 0.443,\n  \"%\": 0.765,\n  \".\": 0.276,\n  \",\": 0.273,\n  x: 0.593,\n  \"×\": 0.593,\n  $: 0.609,\n};\nconst NUM_TRACK = -0.03;\n\nfunction numWidth(text: string, size: number) {\n  let w = 0;\n  for (const ch of text)\n    w += (/\\d/.test(ch) ? 0.62 : (NUM_ADV[ch] ?? 0.6)) + NUM_TRACK;\n  return (w - NUM_TRACK) * size;\n}\n\nconst MAX_ROWS = 14;\nconst START = 6;\nconst COL_STAGGER = 5;\nconst PIXEL_STEP = 1.6;\nconst POP_DUR = 4;\nconst LEAD = 6;\n\nfunction parseMetric(metric: string) {\n  const m = /^(\\D*?)(\\d[\\d,]*(?:\\.\\d+)?)(.*)$/.exec(metric);\n  if (!m) return null;\n  const raw = m[2].replace(/,/g, \"\");\n  const decimals = raw.includes(\".\") ? raw.split(\".\")[1].length : 0;\n  return { prefix: m[1], value: Number(raw), decimals, suffix: m[3] };\n}\n\nexport function PixelBars({\n  values = DEFAULT_VALUES,\n  label = \"this quarter\",\n  metric = \"+64%\",\n  accent = \"#3B5BFD\",\n  theme = \"dark\",\n  speed = 1,\n}: PixelBarsProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H } = useVideoConfig();\n  const u = H / 720;\n  const uid = useId().replace(/[^a-zA-Z0-9_-]/g, \"\");\n  const pal = THEMES[theme];\n\n  const list = values.length ? values.slice(0, 16) : DEFAULT_VALUES;\n  const maxV = Math.max(...list.map((v) => Math.max(0, v)), 1);\n  const heights = list.map((v) =>\n    Math.max(1, Math.round((Math.max(0, v) / maxV) * MAX_ROWS)),\n  );\n  const last = heights.length - 1;\n  const prevMax = Math.max(1, ...heights.slice(0, last));\n\n  const P = (H * 0.58) / MAX_ROWS;\n  const sq = P * 0.82;\n  const colW = P + sq;\n  const colPitch = P * 2.75;\n  const chartW = (heights.length - 1) * colPitch + colW;\n  const chartH = MAX_ROWS * P;\n\n  const numSize = 156 * u;\n  const labelSize = 30 * u;\n  const parsed = parseMetric(metric);\n  const numW = numWidth(metric, numSize);\n  const numGap = 44 * u;\n  const groupW = chartW + numGap + numW;\n  const left = W / 2 - groupW / 2;\n  const base = H / 2 + chartH / 2;\n\n  const colStart = (i: number) => START + i * COL_STAGGER;\n  const pixelTime = (i: number, k: number) => {\n    if (i !== last || k < prevMax) return colStart(i) + k * PIXEL_STEP;\n    const extra = k - prevMax;\n    return (\n      colStart(i) +\n      prevMax * PIXEL_STEP +\n      extra * PIXEL_STEP * 1.3 +\n      extra * extra * 0.14\n    );\n  };\n  const finish = pixelTime(last, heights[last] - 1) + POP_DUR;\n  const passAt = pixelTime(last, prevMax);\n\n  const slots: React.ReactNode[] = [];\n  const cells: React.ReactNode[] = [];\n  heights.forEach((rows, i) => {\n    const x = left + i * colPitch;\n    const isLast = i === last;\n    const slotIn = interpolate(\n      t,\n      [colStart(i) - 6, colStart(i) + 2],\n      [0, 1],\n      CLAMP,\n    );\n    for (let k = 0; k < MAX_ROWS; k++) {\n      const y = base - (k + 1) * P + (P - sq);\n      for (let j = 0; j < 2; j++) {\n        slots.push(\n          <rect\n            key={`s${i}-${k}-${j}`}\n            x={x + j * P}\n            y={y}\n            width={sq}\n            height={sq}\n            fill={pal.slot}\n            opacity={slotIn}\n          />,\n        );\n      }\n      if (k >= rows) continue;\n      const tp = pixelTime(i, k);\n      const age = t - tp;\n      if (age < 0) continue;\n      const pop = POP(interpolate(age, [0, POP_DUR], [0, 1], CLAMP));\n      const on = interpolate(age, [0, 2], [0, 1], CLAMP);\n      const s = sq * (0.7 + 0.3 * pop);\n      const lead = isLast ? 0 : interpolate(age, [0, LEAD], [1, 0], CLAMP);\n      cells.push(\n        <g key={`c${i}-${k}`} opacity={on}>\n          {[0, 1].map((j) => (\n            <rect\n              key={j}\n              x={x + j * P + (sq - s) / 2}\n              y={y + (sq - s) / 2}\n              width={s}\n              height={s}\n              fill={isLast ? `url(#${uid}-hot)` : pal.cell}\n            />\n          ))}\n          {lead > 0\n            ? [0, 1].map((j) => (\n                <rect\n                  key={`l${j}`}\n                  x={x + j * P + (sq - s) / 2}\n                  y={y + (sq - s) / 2}\n                  width={s}\n                  height={s}\n                  fill={pal.lead}\n                  opacity={lead}\n                />\n              ))\n            : null}\n        </g>,\n      );\n    }\n  });\n\n  const lastX = left + last * colPitch;\n  const refY = base - prevMax * P + (P - sq) - (P - sq) / 2;\n  const refK = EASE_OUT(\n    interpolate(t, [passAt - 4, passAt + 8], [0, 1], CLAMP),\n  );\n\n  let litRows = 0;\n  for (let k = 0; k < heights[last]; k++) {\n    const tp = pixelTime(last, k);\n    if (t < tp) break;\n    litRows = Math.max(\n      litRows,\n      k + EASE_OUT(interpolate(t - tp, [0, POP_DUR], [0, 1], CLAMP)),\n    );\n  }\n  const topY = base - litRows * P + (P - sq);\n  const countK = EASE_OUT(\n    interpolate(t, [passAt - 6, finish + 2], [0, 1], CLAMP),\n  );\n  const numIn = EASE_OUT(\n    interpolate(t, [passAt - 2, passAt + 6], [0, 1], CLAMP),\n  );\n  const shown = parsed\n    ? `${parsed.prefix}${(parsed.value * countK).toLocaleString(\"en-US\", {\n        minimumFractionDigits: parsed.decimals,\n        maximumFractionDigits: parsed.decimals,\n      })}${parsed.suffix}`\n    : metric;\n  const numY = topY - numSize * 0.146 + (1 - numIn) * 14 * u;\n  const labelK = EASE_OUT(interpolate(t, [finish, finish + 14], [0, 1], CLAMP));\n  // Colour turns hot exactly where the hero column passes the previous best.\n  const passK = Math.min(0.9, prevMax / heights[last]);\n  const baseK = EASE_OUT(interpolate(t, [0, 14], [0, 1], CLAMP));\n  const cam = interpolate(t, [0, finish + 30], [1.035, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  // Chart alone sits centred; the frame slides over as the number arrives.\n  const slide = EASE_IO(\n    interpolate(t, [passAt - 10, finish + 8], [0, 1], CLAMP),\n  );\n  const camX = ((numGap + numW) / 2) * (1 - slide);\n\n  return (\n    <AbsoluteFill style={{ background: pal.bg, overflow: \"hidden\" }}>\n      <AbsoluteFill\n        style={{\n          background: `radial-gradient(ellipse 55% 60% at 50% 48%, ${pal.light}, transparent 75%)`,\n        }}\n      />\n      <AbsoluteFill\n        style={{\n          transform: `translateX(${camX}px) scale(${cam})`,\n          transformOrigin: \"50% 50%\",\n        }}\n      >\n        <svg\n          width={W}\n          height={H}\n          viewBox={`0 0 ${W} ${H}`}\n          style={{ position: \"absolute\", inset: 0 }}\n          aria-hidden\n        >\n          <defs>\n            <linearGradient\n              id={`${uid}-hot`}\n              gradientUnits=\"userSpaceOnUse\"\n              x1={0}\n              y1={base}\n              x2={0}\n              y2={base - heights[last] * P}\n            >\n              <stop offset={0} stopColor={FLUTED[0]} />\n              <stop offset={passK * 0.55} stopColor={accent} />\n              <stop offset={passK} stopColor={accent} />\n              <stop offset={Math.min(1, passK + 0.1)} stopColor={FLUTED[2]} />\n              <stop offset={1} stopColor={pal.top} />\n            </linearGradient>\n          </defs>\n          {slots}\n          <rect\n            x={left - 10 * u}\n            y={base + 10 * u}\n            width={(chartW + 20 * u) * baseK}\n            height={1.25 * u}\n            fill={pal.hair}\n          />\n          <line\n            x1={left - 10 * u}\n            x2={left - 10 * u + (lastX - left + colW + 20 * u) * refK}\n            y1={refY}\n            y2={refY}\n            stroke={pal.hair}\n            strokeWidth={1.25 * u}\n            strokeDasharray={`${3 * u} ${5 * u}`}\n            opacity={refK}\n          />\n          {cells}\n        </svg>\n        <div\n          style={{\n            position: \"absolute\",\n            left: lastX + colW + numGap,\n            top: numY,\n            opacity: numIn,\n            fontFamily: grotesk,\n            fontWeight: 500,\n            fontSize: numSize,\n            lineHeight: 1,\n            letterSpacing: `${NUM_TRACK}em`,\n            fontVariantNumeric: \"tabular-nums\",\n            color: pal.fg,\n            whiteSpace: \"nowrap\",\n          }}\n        >\n          {shown}\n        </div>\n        {label ? (\n          <div\n            key={uid}\n            style={{\n              position: \"absolute\",\n              left: lastX + colW + numGap + 6 * u,\n              top: numY + numSize * 1.08,\n              display: \"flex\",\n              alignItems: \"center\",\n              gap: 12 * u,\n              opacity: labelK,\n              transform: `translateY(${(1 - labelK) * 8 * u}px)`,\n              fontFamily: grotesk,\n              fontWeight: 400,\n              fontSize: labelSize,\n              lineHeight: 1.2,\n              letterSpacing: \"-0.005em\",\n              color: pal.muted,\n              whiteSpace: \"nowrap\",\n            }}\n          >\n            <div\n              style={{\n                width: 10 * u,\n                height: 24 * u,\n                background: `linear-gradient(0deg, ${accent}, ${FLUTED[2]})`,\n              }}\n            />\n            {label}\n          </div>\n        ) : null}\n      </AbsoluteFill>\n      <svg\n        width=\"100%\"\n        height=\"100%\"\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          opacity: pal.grain,\n          pointerEvents: \"none\",\n        }}\n        aria-hidden\n      >\n        <filter id={`${uid}-grain`}>\n          <feTurbulence\n            type=\"fractalNoise\"\n            baseFrequency=\"0.9\"\n            numOctaves={2}\n            seed={17}\n            stitchTiles=\"stitch\"\n          />\n          <feColorMatrix values=\"0 0 0 0 0.5  0 0 0 0 0.5  0 0 0 0 0.5  0 0 0 0.9 0\" />\n        </filter>\n        <rect width=\"100%\" height=\"100%\" filter={`url(#${uid}-grain)`} />\n      </svg>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/pixel-bars.tsx"
    }
  ],
  "type": "registry:component"
}
