{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "hex-fill",
  "title": "Hex Fill",
  "description": "A hexagonal grid fills with gradient cells up to an 86% reading.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/hex-fill/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont } from \"@remotion/google-fonts/BricolageGrotesque\";\nimport { useId, useMemo } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  spring,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst { fontFamily: brico } = loadFont(\"normal\", {\n  weights: [\"300\", \"400\"],\n  subsets: [\"latin\"],\n});\n\n// gradients.sh \"aurora\" palette\nconst AURORA = {\n  bg: \"#03050b\",\n  c: [\"#a0f0c8\", \"#3fb5a3\", \"#5147b6\", \"#b25c9c\"],\n} as const;\n\nexport interface HexFillProps {\n  value?: number;\n  label?: string;\n  rings?: number;\n  accent?: string;\n  theme?: \"dark\" | \"light\";\n  speed?: number;\n}\n\nconst FILL0 = 14;\nconst FILL_LEN = 70;\nconst RING_LAG = 1.2;\nconst CELL = 0.88;\n\nconst EASE_IO = Easing.bezier(0.65, 0, 0.35, 1);\nconst EASE_OUT = Easing.bezier(0.22, 1, 0.36, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\nconst THEMES = {\n  dark: {\n    bg: AURORA.bg,\n    fg: \"#F1F3F2\",\n    muted: \"#8A8F99\",\n    hair: \"rgba(255,255,255,0.26)\",\n    stops: [AURORA.c[0], AURORA.c[1], AURORA.c[2], AURORA.c[3]],\n    grain: 0.06,\n  },\n  light: {\n    bg: \"#F5F4F0\",\n    fg: \"#121214\",\n    muted: \"#6E6C67\",\n    hair: \"rgba(18,18,20,0.24)\",\n    stops: [\"#52c7a4\", AURORA.c[1], AURORA.c[2], AURORA.c[3]],\n    grain: 0.045,\n  },\n} as const;\n\ninterface Cell {\n  q: number;\n  r: number;\n  x: number;\n  y: number;\n  ring: number;\n  ang: number;\n}\n\nfunction buildCells(rings: number): Cell[] {\n  const out: Cell[] = [];\n  for (let q = -rings; q <= rings; q++) {\n    for (let r = -rings; r <= rings; r++) {\n      const s = -q - r;\n      const ring = Math.max(Math.abs(q), Math.abs(r), Math.abs(s));\n      if (ring > rings) continue;\n      const x = Math.sqrt(3) * (q + r / 2);\n      const y = 1.5 * r;\n      let ang = ring === 0 ? -1 : Math.atan2(x, -y);\n      if (ring > 0 && ang < -1e-6) ang += Math.PI * 2;\n      out.push({ q, r, x, y, ring, ang: ring === 0 ? 0 : ang / (Math.PI * 2) });\n    }\n  }\n  out.sort((a, b) => a.ang - b.ang || a.ring - b.ring);\n  return out;\n}\n\nconst NB = [\n  [1, 0],\n  [-1, 0],\n  [0, 1],\n  [0, -1],\n  [1, -1],\n  [-1, 1],\n];\n\nfunction pickFill(cells: Cell[], F: number) {\n  const index = new Map<string, number>();\n  cells.forEach((c, i) => {\n    index.set(`${c.q},${c.r}`, i);\n  });\n  const nbs = cells.map((c) =>\n    NB.map(([dq, dr]) => index.get(`${c.q + dq},${c.r + dr}`)).filter(\n      (v): v is number => v !== undefined,\n    ),\n  );\n  const on = cells.map((_, i) => i < F);\n  const score = (i: number) =>\n    nbs[i].reduce((sum, j) => sum + (on[j] ? 1 : 0), 0) / nbs[i].length;\n  for (let it = 0; it < 40; it++) {\n    let worst = -1;\n    let best = -1;\n    for (let i = 1; i < cells.length; i++) {\n      if (on[i] && (worst < 0 || score(i) < score(worst))) worst = i;\n      if (!on[i] && (best < 0 || score(i) > score(best))) best = i;\n    }\n    if (worst < 0 || best < 0 || score(best) - score(worst) < 0.34) break;\n    on[worst] = false;\n    on[best] = true;\n  }\n  const chosen = cells.filter((_, i) => on[i]);\n  const rest = cells.filter((_, i) => !on[i]);\n  return [...chosen, ...rest];\n}\n\nfunction hexPath(cx: number, cy: number, r: number) {\n  let d = \"\";\n  for (let i = 0; i < 6; i++) {\n    const a = (Math.PI / 3) * i - Math.PI / 2;\n    d += `${i === 0 ? \"M\" : \"L\"}${(cx + r * Math.cos(a)).toFixed(2)} ${(cy + r * Math.sin(a)).toFixed(2)}`;\n  }\n  return `${d}Z`;\n}\n\nfunction Grain({ opacity }: { opacity: number }) {\n  const id = `g${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n  return (\n    <svg\n      width=\"100%\"\n      height=\"100%\"\n      style={{ position: \"absolute\", inset: 0, opacity }}\n      aria-hidden\n    >\n      <filter id={id}>\n        <feTurbulence\n          type=\"fractalNoise\"\n          baseFrequency=\"0.9\"\n          numOctaves={2}\n          seed={53}\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(#${id})`} />\n    </svg>\n  );\n}\n\nexport function HexFill({\n  value = 86,\n  label = \"of renders served from cache\",\n  rings = 7,\n  accent = \"#3B5BFD\",\n  theme = \"dark\",\n  speed = 1,\n}: HexFillProps) {\n  const t = useCurrentFrame() * speed;\n  const { width, height, fps } = useVideoConfig();\n  const u = Math.min(width / 1280, height / 720);\n  const p = THEMES[theme];\n  const N = Math.max(3, Math.min(10, Math.round(rings)));\n  const pct = Math.max(0, Math.min(100, value));\n  const all = useMemo(() => buildCells(N), [N]);\n  const F = Math.round((pct / 100) * all.length);\n  const cells = useMemo(() => pickFill(all, F), [all, F]);\n\n  const heroSize = 168 * u;\n  const pctSize = heroSize * 0.55;\n  const labelSize = 24 * u;\n  const digits = String(Math.round(pct)).length;\n  const heroW = digits * 0.6 * heroSize + 0.62 * pctSize;\n  const labelMax = 300 * u;\n  const labelW = Math.min(labelMax, label.length * 0.5 * labelSize) + 22 * u;\n  const textW = Math.max(heroW, labelW);\n  const gap = 80 * u;\n\n  const gridH = 3 * N + 2;\n  const gridWUnits = (2 * N + 1) * Math.sqrt(3);\n  const s = Math.min(\n    (height * 0.74) / gridH,\n    (width * 0.86 - gap - textW) / gridWUnits,\n  );\n  const gridW = gridWUnits * s;\n  const left = (width - (gridW + gap + textW)) / 2;\n  const gcx = left + gridW / 2;\n  const gcy = height / 2;\n  const textX = left + gridW + gap;\n\n  const angMax = F > 0 ? cells[F - 1].ang : 1;\n  const fillAt = (i: number) => {\n    const k = Math.min(1, cells[i].ang / Math.max(1e-3, angMax));\n    return (\n      FILL0 +\n      (FILL_LEN * Math.acos(1 - 2 * k)) / Math.PI +\n      cells[i].ring * RING_LAG\n    );\n  };\n  let fillEnd = FILL0;\n  let filled = 0;\n  for (let i = 0; i < F; i++) {\n    const at = fillAt(i);\n    fillEnd = Math.max(fillEnd, at);\n    if (t >= at) filled++;\n  }\n  const shown = F > 0 ? Math.round((pct * filled) / F) : 0;\n\n  const push = interpolate(t, [0, fillEnd + 40], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const camScale = 0.985 + 0.025 * push;\n  const textIn = interpolate(t, [4, 22], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const labelIn = interpolate(t, [10, 28], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const done = interpolate(t, [fillEnd + 2, fillEnd + 18], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const hexR = s * CELL;\n  const gradId = `hf${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n  const gR = (gridH * s) / 2;\n  const drift = interpolate(t, [0, fillEnd + 40], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const sw = Math.max(1, 1.1 * u);\n\n  return (\n    <AbsoluteFill style={{ background: p.bg, overflow: \"hidden\" }}>\n      <AbsoluteFill\n        style={{\n          transform: `scale(${camScale})`,\n          transformOrigin: \"50% 50%\",\n        }}\n      >\n        <svg\n          width={width}\n          height={height}\n          style={{ position: \"absolute\", inset: 0 }}\n          aria-hidden\n        >\n          <defs>\n            <linearGradient\n              id={gradId}\n              gradientUnits=\"userSpaceOnUse\"\n              x1={gcx - gR * (0.9 - 0.15 * drift)}\n              y1={gcy - gR}\n              x2={gcx + gR * (0.9 + 0.15 * drift)}\n              y2={gcy + gR}\n            >\n              <stop offset=\"0\" stopColor={p.stops[0]} />\n              <stop offset=\"0.36\" stopColor={p.stops[1]} />\n              <stop offset=\"0.74\" stopColor={p.stops[2]} />\n              <stop offset=\"1\" stopColor={p.stops[3]} />\n            </linearGradient>\n          </defs>\n          {cells.map((c, i) => {\n            const x = gcx + c.x * s;\n            const y = gcy + c.y * s;\n            const ringIn = interpolate(\n              t,\n              [c.ring * 1.4, c.ring * 1.4 + 10],\n              [0, 1],\n              {\n                ...CLAMP,\n                easing: EASE_OUT,\n              },\n            );\n            const isFill = i < F;\n            const local = isFill ? t - fillAt(i) : -1;\n            const pop =\n              local >= 0\n                ? spring({\n                    frame: local,\n                    fps,\n                    config: { damping: 13, stiffness: 240, mass: 0.6 },\n                  })\n                : 0;\n            const outlineA = isFill ? ringIn : ringIn * (1 - 0.2 * done);\n            return (\n              <g key={`${c.x.toFixed(2)}_${c.y.toFixed(2)}`}>\n                <path\n                  d={hexPath(x, y, hexR * (0.94 + 0.06 * ringIn))}\n                  fill=\"none\"\n                  stroke={p.hair}\n                  strokeWidth={sw}\n                  opacity={\n                    local >= 0\n                      ? outlineA * (1 - Math.min(1, local / 4))\n                      : outlineA\n                  }\n                />\n                {local >= 0 ? (\n                  <path\n                    d={hexPath(x, y, hexR * (0.55 + 0.45 * pop))}\n                    fill={`url(#${gradId})`}\n                  />\n                ) : null}\n              </g>\n            );\n          })}\n        </svg>\n\n        <div\n          style={{\n            position: \"absolute\",\n            left: textX,\n            top: gcy,\n            transform: \"translateY(-50%)\",\n            display: \"flex\",\n            flexDirection: \"column\",\n            alignItems: \"flex-start\",\n            gap: 18 * u,\n          }}\n        >\n          <div\n            style={{\n              fontFamily: brico,\n              fontWeight: 300,\n              fontSize: heroSize,\n              lineHeight: 0.9,\n              letterSpacing: \"-0.05em\",\n              color: p.fg,\n              fontVariantNumeric: \"tabular-nums\",\n              whiteSpace: \"nowrap\",\n              opacity: textIn,\n              transform: `translateY(${(1 - textIn) * 14 * u}px)`,\n            }}\n          >\n            {shown}\n            <span\n              style={{\n                fontSize: pctSize,\n                letterSpacing: \"-0.02em\",\n                marginLeft: 6 * u,\n                color: p.muted,\n              }}\n            >\n              %\n            </span>\n          </div>\n          <div\n            style={{\n              display: \"flex\",\n              alignItems: \"flex-start\",\n              gap: 12 * u,\n              marginLeft: 6 * u,\n              maxWidth: labelMax + 22 * u,\n              opacity: labelIn,\n              transform: `translateY(${(1 - labelIn) * 8 * u}px)`,\n            }}\n          >\n            <span\n              style={{\n                width: 10 * u,\n                height: 10 * u,\n                borderRadius: \"50%\",\n                background: accent,\n                flexShrink: 0,\n                marginTop: labelSize * 0.42,\n              }}\n            />\n            <span\n              style={{\n                fontFamily: brico,\n                fontWeight: 400,\n                fontSize: labelSize,\n                lineHeight: 1.3,\n                letterSpacing: \"-0.01em\",\n                color: p.muted,\n                textWrap: \"balance\",\n              }}\n            >\n              {label}\n            </span>\n          </div>\n        </div>\n      </AbsoluteFill>\n\n      <AbsoluteFill\n        style={{\n          background: `radial-gradient(ellipse 85% 85% at 50% 50%, transparent 64%, ${p.bg} 100%)`,\n          opacity: 0.5,\n        }}\n      />\n      <Grain opacity={p.grain} />\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/hex-fill.tsx"
    }
  ],
  "type": "registry:component"
}
