{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "feature-chips-flow",
  "title": "Feature Chips Flow",
  "description": "Rows of feature pills stream by, then snap into a cluster around the logo.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/feature-chips-flow/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadGeist } from \"@remotion/google-fonts/Geist\";\nimport { loadFont as loadManrope } from \"@remotion/google-fonts/Manrope\";\nimport type React from \"react\";\nimport { useId, useMemo } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  spring,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst { fontFamily: GEIST } = loadGeist(\"normal\", {\n  weights: [\"400\", \"500\"],\n  subsets: [\"latin\"],\n});\nconst manrope = loadManrope(\"normal\", {\n  weights: [\"500\", \"600\"],\n  subsets: [\"latin\"],\n});\n\n/** Geist 500 advance widths, ASCII 32–126, per 1000 units (measured from the font). */\nconst GEIST_ADV = [\n  243, 228, 361, 515, 643, 810, 649, 186, 290, 290, 427, 562, 213, 418, 213,\n  494, 673, 406, 630, 625, 629, 641, 604, 531, 624, 606, 302, 302, 546, 544,\n  546, 570, 925, 689, 688, 713, 701, 609, 595, 713, 716, 280, 607, 656, 583,\n  890, 745, 751, 657, 745, 680, 654, 568, 694, 688, 968, 633, 594, 561, 361,\n  470, 361, 438, 558, 258, 565, 608, 563, 608, 576, 412, 607, 591, 256, 284,\n  609, 282, 885, 591, 588, 608, 608, 394, 537, 410, 586, 560, 829, 607, 553,\n  552, 395, 274, 395, 523,\n];\nconst CHIP_TRACKING = -0.01;\n\nfunction geistWidth(text: string, size: number): number {\n  let w = 0;\n  for (const ch of text) {\n    const c = ch.charCodeAt(0);\n    w += c >= 32 && c < 127 ? GEIST_ADV[c - 32] : 600;\n  }\n  return (w / 1000 + CHIP_TRACKING * text.length) * size;\n}\n\nconst REMOCN_R_PATH =\n  \"M 0.01 0.81 C 0.01 1.73, 0.36 2.79, 1.09 4.13 C 4.91 11.04, 13.45 17.16, 21.7 18.9 C 22.94 19.16, 23.18 19.16, 51.39 19.27 C 76.07 19.36, 80.02 19.4, 81.32 19.57 C 89.89 20.69, 96.2 24.68, 99.38 31.01 C 103.19 38.56, 102.53 50.1, 97.91 57.07 C 94.66 61.96, 89.68 64.99, 83.26 66 C 82.81 66.07, 70.18 66.83, 55.2 67.69 C 24.82 69.43, 27.03 69.23, 24.18 70.4 C 19.9 72.15, 14.84 75.7, 10.65 79.89 C 4.86 85.68, 1.3 91.91, 0.25 98.13 C 0.12 98.85, 0.08 103.13, 0.04 116.66 L 0 134.26 9.5 134.26 L 19 134.26 19.05 119.41 C 19.1 103.27, 19.08 103.82, 19.82 101.04 C 21.79 93.65, 27.86 88.75, 36.45 87.63 C 37.23 87.53, 39.41 87.5, 43.57 87.53 C 50.12 87.59, 50.75 87.65, 53.22 88.45 C 56.61 89.56, 59.67 91.86, 62.02 95.07 C 62.52 95.76, 69.35 103.85, 77.2 113.07 C 85.04 122.28, 91.63 130.04, 91.85 130.32 C 92.07 130.59, 92.5 131.34, 92.82 131.97 C 93.52 133.35, 94.11 133.93, 95.06 134.13 C 95.5 134.23, 98.97 134.26, 106.36 134.23 L 117.01 134.19 100.82 113.07 C 91.91 101.45, 84.52 91.78, 84.39 91.57 C 83.36 89.89, 83.66 87.53, 85.09 86 C 85.79 85.25, 86.36 84.94, 88.07 84.38 C 96.18 81.72, 104.15 76.62, 109.97 70.36 C 120.59 58.93, 124.06 44.32, 119.44 30.43 C 114.59 15.81, 101.93 4.02, 87.64 0.83 C 83.93 0.01, 88.09 0.08, 41.85 0.04 L 0.01 0 0.01 0.81 Z\";\n\n/** Full logo as on remocn.dev: R mark (0.76em tall, baseline-aligned) + \"emocn\" in Manrope 600, tracking-tight. */\nfunction RemocnLogo({ fontSize, color }: { fontSize: number; color: string }) {\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        alignItems: \"baseline\",\n        color,\n        fontFamily: manrope.fontFamily,\n        fontWeight: 600,\n        fontSize,\n        letterSpacing: \"-0.025em\",\n        lineHeight: 1,\n      }}\n    >\n      <svg\n        viewBox=\"0 0 124.06 134.26\"\n        style={{ height: \"0.76em\", width: \"auto\" }}\n        aria-hidden\n      >\n        <path fill=\"currentColor\" d={REMOCN_R_PATH} />\n      </svg>\n      <span style={{ marginLeft: \"0.04em\" }}>emocn</span>\n    </div>\n  );\n}\n\n/** Logo width in em (R 0.702 + gap 0.04 + \"emocn\" at -0.025em), measured from Manrope 600. */\nconst LOGO_EM = 3.86;\n\n/**\n * Beat sheet (30fps, speed 1):\n * 0–58     seven rows of pills stream left like a river; near rows big, fast\n *          and soft, far rows small, slow and blurred.\n * 58–100   the headliners peel out of the stream (nearest first) and snap\n *          into a tight lozenge around an empty centre; the rest dissolve.\n * 96       the remocn.dev logo lands in the hole: a short, confident hit that\n *          nudges the cluster outward.\n * Graphics: Geist 500 chips; the gradients.sh \"beam\" palette fills the accent\n * chips and pools softly under the cluster.\n * 100–170  hold, slow push-in.\n */\n\nexport type ChipIcon =\n  | \"captions\"\n  | \"export\"\n  | \"transitions\"\n  | \"palette\"\n  | \"grid\"\n  | \"spring\"\n  | \"wave\"\n  | \"type\"\n  | \"sun\"\n  | \"layers\"\n  | \"cursor\"\n  | \"phone\"\n  | \"camera\"\n  | \"code\"\n  | \"moon\"\n  | \"clock\"\n  | \"terminal\"\n  | \"play\"\n  | \"image\"\n  | \"chart\";\n\nexport interface FeatureChip {\n  label: string;\n  icon?: ChipIcon;\n  variant?: \"neutral\" | \"accent\" | \"outline\";\n}\n\nexport interface FeatureChipsFlowProps {\n  /** The first ~22 land in the cluster; the rest stream by and dissolve. */\n  features?: (string | FeatureChip)[];\n  /** \"remocn\" (default) renders the real remocn.dev logo; anything else is set as type. */\n  name?: string;\n  /** Shown before a custom `name` (ignored for the built-in logo). */\n  logo?: React.ReactNode;\n  accent?: string;\n  speed?: number;\n  theme?: \"dark\" | \"light\";\n}\n\nconst DEFAULT_FEATURES: FeatureChip[] = [\n  { label: \"Captions\", icon: \"captions\", variant: \"accent\" },\n  { label: \"Transitions\", icon: \"transitions\" },\n  { label: \"4K export\", icon: \"export\", variant: \"outline\" },\n  { label: \"Brand kit\", icon: \"palette\" },\n  { label: \"Templates\", icon: \"grid\" },\n  { label: \"Springs\", icon: \"spring\", variant: \"outline\" },\n  { label: \"Audio sync\", icon: \"wave\" },\n  { label: \"Kinetic type\", icon: \"type\" },\n  { label: \"Light leaks\", icon: \"sun\", variant: \"outline\" },\n  { label: \"Backgrounds\", icon: \"layers\" },\n  { label: \"Cursor moves\", icon: \"cursor\" },\n  { label: \"Vertical video\", icon: \"phone\", variant: \"outline\" },\n  { label: \"Camera moves\", icon: \"camera\" },\n  { label: \"Own your code\", icon: \"code\", variant: \"accent\" },\n  { label: \"Dark mode\", icon: \"moon\", variant: \"outline\" },\n  { label: \"60 fps\", icon: \"clock\" },\n  { label: \"shadcn CLI\", icon: \"terminal\" },\n  { label: \"Remotion Studio\", icon: \"play\", variant: \"outline\" },\n  { label: \"Screenshots\", icon: \"image\" },\n  { label: \"Charts\", icon: \"chart\" },\n  { label: \"Lower thirds\", icon: \"captions\", variant: \"outline\" },\n  { label: \"Logo reveals\", icon: \"sun\" },\n  { label: \"Transcripts\", icon: \"type\" },\n  { label: \"Device frames\", icon: \"phone\", variant: \"outline\" },\n  { label: \"Countdowns\", icon: \"clock\" },\n  { label: \"Code blocks\", icon: \"code\" },\n  { label: \"Grain\", icon: \"layers\", variant: \"outline\" },\n  { label: \"Alpha channel\", icon: \"layers\" },\n  { label: \"Easing curves\", icon: \"spring\" },\n  { label: \"Social cuts\", icon: \"phone\", variant: \"outline\" },\n  { label: \"Chapters\", icon: \"grid\" },\n  { label: \"MP4 + WebM\", icon: \"export\" },\n  { label: \"Subtitles\", icon: \"captions\", variant: \"outline\" },\n  { label: \"Zoom blur\", icon: \"camera\" },\n  { label: \"Color themes\", icon: \"palette\" },\n  { label: \"Typewriter\", icon: \"type\", variant: \"outline\" },\n  { label: \"Waveforms\", icon: \"wave\" },\n  { label: \"Data stories\", icon: \"chart\", variant: \"outline\" },\n  { label: \"Previews\", icon: \"play\" },\n  { label: \"Paper textures\", icon: \"image\" },\n  { label: \"Loops\", icon: \"spring\", variant: \"outline\" },\n  { label: \"Stills\", icon: \"image\" },\n];\n\nconst ICON_CYCLE: ChipIcon[] = [\n  \"grid\",\n  \"layers\",\n  \"spring\",\n  \"type\",\n  \"wave\",\n  \"play\",\n  \"code\",\n];\n\n/** Cluster rows: chips per row, top to bottom; the middle row wraps the name. */\nconst CLUSTER_ROWS = [4, 5, 4, 5, 4];\nconst NAME_ROW = 2;\n\n/** Rows of the river: screen position (0–1) and depth (0 far, 1 near). */\nconst RIVER_DEPTHS = [0.4, 0.78, 0.12, 1, 0.55, 0.22, 0.86];\n\nconst CONVERGE = 58;\nconst HIT = 96;\n\nconst OUT = Easing.bezier(0.22, 1, 0.36, 1);\nconst INOUT = Easing.bezier(0.65, 0, 0.35, 1);\n\nfunction hash(n: number): number {\n  const s = Math.sin(n * 127.1 + 311.7) * 43758.5453;\n  return s - Math.floor(s);\n}\n\nconst ICONS: Record<ChipIcon, string> = {\n  captions: \"M4 6h16v10H4z M7 10h6 M7 13h10\",\n  export: \"M12 4v11 M7.5 8.5L12 4l4.5 4.5 M5 15v4h14v-4\",\n  transitions: \"M4 5h10v10H4z M10 9h10v10H10z\",\n  palette:\n    \"M12 4a8 8 0 1 0 0 16c1.5 0 2-1 1.4-2.2-.6-1.2.2-2.3 1.6-2.3H17a3 3 0 0 0 3-3 8 8 0 0 0-8-8.5z M8 11h.01 M11 8h.01 M15 9h.01\",\n  grid: \"M4 4h7v7H4z M13 4h7v7h-7z M4 13h7v7H4z M13 13h7v7h-7z\",\n  spring: \"M3 17c4 0 4-10 9-10s5 10 9 10\",\n  wave: \"M4 10v4 M8 7v10 M12 4v16 M16 8v8 M20 10v4\",\n  type: \"M5 6h14 M12 6v13 M9 19h6\",\n  sun: \"M12 8a4 4 0 1 0 0 8a4 4 0 1 0 0-8z M12 2.5v2 M12 19.5v2 M2.5 12h2 M19.5 12h2 M5.3 5.3l1.4 1.4 M17.3 17.3l1.4 1.4 M5.3 18.7l1.4-1.4 M17.3 6.7l1.4-1.4\",\n  layers: \"M12 4l8 4-8 4-8-4z M4 12l8 4 8-4 M4 16l8 4 8-4\",\n  cursor: \"M5 4l12 6-5.5 1.5L10 17z\",\n  phone:\n    \"M8 3h8a1.5 1.5 0 0 1 1.5 1.5v15A1.5 1.5 0 0 1 16 21H8a1.5 1.5 0 0 1-1.5-1.5v-15A1.5 1.5 0 0 1 8 3z M11 18h2\",\n  camera: \"M4 8h3l2-2.5h6L17 8h3v11H4z M12 10.5a3 3 0 1 0 0 6a3 3 0 1 0 0-6z\",\n  code: \"M9 7l-5 5 5 5 M15 7l5 5-5 5\",\n  moon: \"M19 14.5A7.5 7.5 0 0 1 9.5 5a7.5 7.5 0 1 0 9.5 9.5z\",\n  clock: \"M12 4a8 8 0 1 0 0 16a8 8 0 1 0 0-16z M12 8v4l3 2\",\n  terminal: \"M4 5h16v14H4z M7.5 9.5l3 2.5-3 2.5 M12.5 15h4\",\n  play: \"M12 4a8 8 0 1 0 0 16a8 8 0 1 0 0-16z M10.5 9v6l4.5-3z\",\n  image: \"M4 5h16v14H4z M4 16l5-5 4 4 2-2 5 5 M15 9h.01\",\n  chart: \"M4 20h16 M7 16v-4 M11 16V8 M15 16v-6 M19 16V5\",\n};\n\nconst ChipGlyph: React.FC<{ icon: ChipIcon; size: number; color: string }> = ({\n  icon,\n  size,\n  color,\n}) => (\n  <svg width={size} height={size} viewBox=\"0 0 24 24\" aria-hidden>\n    <path\n      d={ICONS[icon]}\n      fill=\"none\"\n      stroke={color}\n      strokeWidth={1.8}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    />\n  </svg>\n);\n\ninterface Placed {\n  chip: FeatureChip;\n  w: number;\n  row: number;\n  depth: number;\n  rowY: number;\n  x0: number;\n  period: number;\n  v: number;\n  target?: { x: number; y: number };\n}\n\nexport function FeatureChipsFlow({\n  features = DEFAULT_FEATURES,\n  name = \"remocn\",\n  logo,\n  accent = \"#3B5BFD\",\n  speed = 1,\n  theme = \"dark\",\n}: FeatureChipsFlowProps) {\n  const frame = useCurrentFrame() * speed;\n  const { width: W, height: H, fps } = useVideoConfig();\n  const uid = useId().replace(/[^a-zA-Z0-9_-]/g, \"\");\n  const isLight = theme === \"light\";\n  const isLogo = name.trim().toLowerCase() === \"remocn\";\n\n  const p = isLight\n    ? {\n        stage: \"#F5F4F0\",\n        top: \"#FCFBF8\",\n        chip: \"#FFFFFF\",\n        hair: \"rgba(20,20,22,0.10)\",\n        outline: \"rgba(20,20,22,0.22)\",\n        ink: \"#141416\",\n        muted: \"rgba(20,20,22,0.62)\",\n        shadow: \"0 1px 2px rgba(40,36,28,0.08), 0 6px 18px rgba(40,36,28,0.06)\",\n      }\n    : {\n        stage: \"#0A0A0B\",\n        top: \"#141417\",\n        chip: \"#18181B\",\n        hair: \"rgba(255,255,255,0.08)\",\n        outline: \"rgba(255,255,255,0.2)\",\n        ink: \"#EDEDEF\",\n        muted: \"rgba(237,237,239,0.62)\",\n        shadow: \"0 6px 18px rgba(0,0,0,0.35)\",\n      };\n\n  const chipH = H * 0.06;\n  const font = chipH * 0.36;\n  const iconSize = chipH * 0.4;\n  const padX = chipH * 0.38;\n  const gap = chipH * 0.22;\n\n  const chips: FeatureChip[] = useMemo(\n    () =>\n      features.map((f, i) =>\n        typeof f === \"string\"\n          ? {\n              label: f,\n              icon: ICON_CYCLE[i % ICON_CYCLE.length],\n              variant: i % 4 === 2 ? \"outline\" : \"neutral\",\n            }\n          : f,\n      ),\n    [features],\n  );\n\n  const placed = useMemo(() => {\n    const rows = RIVER_DEPTHS.length;\n    const widthOf = (c: FeatureChip) =>\n      padX * 2 + iconSize + gap + geistWidth(c.label, font);\n    const list: Placed[] = chips.map((chip, i) => {\n      const row = i % rows;\n      const depth = RIVER_DEPTHS[row];\n      return {\n        chip,\n        w: widthOf(chip),\n        row,\n        depth,\n        rowY: H * (0.09 + (row / (rows - 1)) * 0.82),\n        x0: 0,\n        period: 0,\n        v: 1.1 + depth * 4.2,\n      };\n    });\n    // Spread each row evenly across a loop a little wider than the frame.\n    for (let r = 0; r < rows; r++) {\n      const inRow = list.filter((c) => c.row === r);\n      const s = 0.62 + RIVER_DEPTHS[r] * 0.55;\n      const total = inRow.reduce((sum, c) => sum + c.w * s, 0);\n      const period = Math.max(W * 1.3, total + inRow.length * chipH * 0.6);\n      let x = hash(r * 3.7) * period;\n      for (const c of inRow) {\n        c.period = period;\n        c.x0 = x + (c.w * s) / 2;\n        x += (period / inRow.length) * (0.85 + hash(c.x0) * 0.3);\n      }\n    }\n\n    // Headliners: the first chips land in the cluster. Order them by where\n    // they will be in the river when the converge starts, so rows map to rows.\n    const capacity = CLUSTER_ROWS.reduce((a, b) => a + b, 0);\n    const heads = list.slice(0, Math.min(capacity, list.length));\n    const flowX = (c: Placed, t: number) => {\n      const s = 0.62 + c.depth * 0.55;\n      const raw = c.x0 - c.v * t;\n      const span = c.period;\n      return (((raw % span) + span) % span) - (c.w * s) / 2 - span * 0.12;\n    };\n    const ordered = [...heads].sort(\n      (a, b) => a.rowY - b.rowY || flowX(a, CONVERGE) - flowX(b, CONVERGE),\n    );\n    const nameW = isLogo\n      ? H * 0.13 * LOGO_EM\n      : geistWidth(name, H * 0.13) * 0.97 + (logo ? H * 0.13 * 0.8 : 0);\n    const rowGap = chipH * 1.32;\n    let k = 0;\n    CLUSTER_ROWS.forEach((count, r) => {\n      const members = ordered\n        .slice(k, k + count)\n        .sort((a, b) => flowX(a, CONVERGE) - flowX(b, CONVERGE));\n      k += count;\n      const dy =\n        (r - NAME_ROW) * rowGap +\n        Math.sign(r - NAME_ROW) * chipH * (r === NAME_ROW ? 0 : 0.38);\n      const y = H * 0.5 + dy;\n      if (r === NAME_ROW) {\n        const half = Math.ceil(members.length / 2);\n        const left = members.slice(0, half);\n        const right = members.slice(half);\n        let x = W / 2 - nameW / 2 - gap * 3;\n        for (const c of [...left].reverse()) {\n          c.target = { x: x - c.w / 2, y };\n          x -= c.w + gap * 1.6;\n        }\n        x = W / 2 + nameW / 2 + gap * 3;\n        for (const c of right) {\n          c.target = { x: x + c.w / 2, y };\n          x += c.w + gap * 1.6;\n        }\n      } else {\n        const total =\n          members.reduce((sum, c) => sum + c.w, 0) +\n          (members.length - 1) * gap * 1.6;\n        let x = W / 2 - total / 2 + (r % 2 ? -chipH * 0.4 : chipH * 0.4);\n        for (const c of members) {\n          c.target = { x: x + c.w / 2, y };\n          x += c.w + gap * 1.6;\n        }\n      }\n    });\n    return { list, flowX };\n  }, [chips, W, H, chipH, font, iconSize, padX, gap, name, isLogo, logo]);\n\n  // The hit: name lands, the cluster takes a small outward nudge and settles.\n  const hitT = frame - HIT;\n  const nameIn = interpolate(hitT, [-2, 8], [0, 1], {\n    easing: OUT,\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n  const nameScale = interpolate(hitT, [-2, 10], [1.1, 1], {\n    easing: OUT,\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n  const nudge =\n    hitT > 0\n      ? Math.sin(hitT * 0.42) * Math.exp(-hitT * 0.19) * chipH * 0.16\n      : 0;\n\n  // gradients.sh \"beam\" palette: bg #02030a, [#f4f7ff, #8fb4ff, #3d5bff, #1a1660].\n  // It fills the accent chips and pools under the cluster; both drift gently.\n  const drift = interpolate(frame, [0, 170], [0, 1], {\n    extrapolateRight: \"clamp\",\n  });\n  const beamChip = `radial-gradient(90% 160% at ${18 + drift * 10}% -10%, rgba(143,180,255,0.75) 0%, rgba(143,180,255,0) 60%), linear-gradient(${100 + drift * 12}deg, ${accent} 0%, #3d5bff 45%, #1a1660 135%)`;\n  const pool = interpolate(frame, [CONVERGE, HIT + 20], [0.55, 1], {\n    easing: INOUT,\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n  const ground = isLight\n    ? `radial-gradient(62% 52% at ${48 + drift * 4}% 56%, rgba(143,180,255,${(0.34 * pool).toFixed(3)}) 0%, rgba(143,180,255,0) 72%), radial-gradient(110% 85% at 50% 45%, ${p.top} 0%, ${p.stage} 70%)`\n    : `radial-gradient(58% 50% at ${48 + drift * 4}% 56%, rgba(61,91,255,${(0.2 * pool).toFixed(3)}) 0%, rgba(26,22,96,${(0.16 * pool).toFixed(3)}) 50%, rgba(2,3,10,0) 78%), radial-gradient(110% 85% at 50% 45%, ${p.top} 0%, ${p.stage} 70%)`;\n\n  const push = interpolate(frame, [CONVERGE, 170], [1, 1.045], {\n    easing: INOUT,\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n\n  const stateAt = (c: Placed, i: number, f: number) => {\n    const s0 = 0.62 + c.depth * 0.55;\n    const fx = placed.flowX(c, f);\n    const fy = c.rowY;\n    const farBlur = (1 - c.depth) ** 1.6 * 3.2 + (c.depth > 0.95 ? 1.1 : 0);\n    const farOp = 0.4 + c.depth * 0.6;\n    if (c.target) {\n      // Nearest headliners peel off first (distance measured at the cue).\n      const cx0 = placed.flowX(c, CONVERGE);\n      const d =\n        Math.hypot(cx0 - c.target.x, fy - c.target.y) / Math.hypot(W, H);\n      const delay = d * 22 + hash(i * 2.1) * 4;\n      const pr = spring({\n        frame: f - CONVERGE - delay,\n        fps,\n        config: { damping: 17, stiffness: 120, mass: 0.8 },\n      });\n      const lin = Math.min(1, Math.max(0, pr));\n      const dx = c.target.x - W / 2;\n      const dy = c.target.y - H / 2;\n      const len = Math.hypot(dx, dy) || 1;\n      return {\n        x: fx + (c.target.x - fx) * pr + (dx / len) * nudge,\n        y: fy + (c.target.y - fy) * pr + (dy / len) * nudge * 0.6,\n        scale: s0 + (1 - s0) * lin,\n        blur: farBlur * (1 - lin),\n        op: farOp + (1 - farOp) * lin,\n        z: 200 + Math.round(lin * 100),\n      };\n    }\n    const out = interpolate(\n      f,\n      [CONVERGE + 4 + hash(i) * 10, CONVERGE + 30 + hash(i) * 10],\n      [0, 1],\n      { extrapolateLeft: \"clamp\", extrapolateRight: \"clamp\", easing: INOUT },\n    );\n    return {\n      x: fx,\n      y: fy,\n      scale: s0 * (1 + out * 0.08 * (c.depth > 0.5 ? 1 : -1)),\n      blur: farBlur + out * 8,\n      op: farOp * (1 - out),\n      z: Math.round(c.depth * 100),\n    };\n  };\n\n  const renderChip = (\n    c: Placed,\n    key: string,\n    st: ReturnType<typeof stateAt>,\n  ) => {\n    const v = c.chip.variant ?? \"neutral\";\n    const isAccent = v === \"accent\";\n    const isOutline = v === \"outline\";\n    const fg = isAccent ? \"#FFFFFF\" : isOutline ? p.muted : p.ink;\n    return (\n      <div\n        key={key}\n        style={{\n          position: \"absolute\",\n          left: 0,\n          top: 0,\n          width: c.w,\n          height: chipH,\n          transform: `translate(${st.x - c.w / 2}px, ${st.y - chipH / 2}px) scale(${st.scale})`,\n          zIndex: st.z,\n          opacity: st.op,\n          filter: st.blur > 0.2 ? `blur(${st.blur.toFixed(2)}px)` : undefined,\n          borderRadius: chipH / 2,\n          background: isAccent ? beamChip : isOutline ? \"transparent\" : p.chip,\n          boxShadow: isAccent\n            ? `inset 0 1px 0 rgba(244,247,255,0.35), inset 0 0 0 1px rgba(244,247,255,0.12), 0 8px 24px rgba(61,91,255,${isLight ? 0.22 : 0.3})`\n            : isOutline\n              ? `inset 0 0 0 1px ${p.outline}`\n              : `inset 0 0 0 1px ${p.hair}, ${p.shadow}`,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          gap,\n          fontFamily: GEIST,\n          fontWeight: 500,\n          fontSize: font,\n          letterSpacing: `${CHIP_TRACKING}em`,\n          color: fg,\n          whiteSpace: \"nowrap\",\n        }}\n      >\n        <ChipGlyph icon={c.chip.icon ?? \"grid\"} size={iconSize} color={fg} />\n        {c.chip.label}\n      </div>\n    );\n  };\n\n  const nodes: React.ReactNode[] = [];\n  placed.list.forEach((c, i) => {\n    const st = stateAt(c, i, frame);\n    if (st.op <= 0.01) return;\n    // Fake motion blur: two faint trailing copies while a chip snaps fast.\n    if (c.target) {\n      const prev = stateAt(c, i, frame - 1);\n      const v = Math.hypot(st.x - prev.x, st.y - prev.y);\n      if (v > chipH * 0.25) {\n        const k = Math.min(1, (v - chipH * 0.25) / (chipH * 0.5));\n        [0.45, 0.9].forEach((back, g) => {\n          const gs = stateAt(c, i, frame - back);\n          nodes.push(\n            renderChip(c, `${i}-g${g}`, {\n              ...gs,\n              op: gs.op * k * (g ? 0.12 : 0.22),\n              blur: gs.blur + 1.5,\n              z: st.z - 1,\n            }),\n          );\n        });\n      }\n    }\n    nodes.push(renderChip(c, `${i}`, st));\n  });\n\n  const nameSize = H * 0.13;\n\n  return (\n    <AbsoluteFill\n      style={{\n        background: ground,\n        overflow: \"hidden\",\n      }}\n    >\n      <AbsoluteFill\n        style={{ transform: `scale(${push})`, transformOrigin: \"50% 50%\" }}\n      >\n        {nodes}\n        <div\n          style={{\n            position: \"absolute\",\n            left: 0,\n            right: 0,\n            top: H * 0.5 - nameSize * 0.62,\n            height: nameSize * 1.24,\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n            gap: nameSize * 0.2,\n            opacity: nameIn,\n            transform: `scale(${nameScale})`,\n            filter: nameIn < 1 ? `blur(${(1 - nameIn) * 10}px)` : undefined,\n            zIndex: 400,\n          }}\n        >\n          {isLogo ? (\n            <RemocnLogo fontSize={nameSize} color={p.ink} />\n          ) : (\n            <>\n              {logo}\n              <span\n                style={{\n                  fontFamily: GEIST,\n                  fontWeight: 500,\n                  fontSize: nameSize,\n                  letterSpacing: \"-0.04em\",\n                  lineHeight: 1,\n                  color: p.ink,\n                  paddingBottom: nameSize * 0.06,\n                }}\n              >\n                {name}\n              </span>\n            </>\n          )}\n        </div>\n      </AbsoluteFill>\n      <svg\n        width={W}\n        height={H}\n        style={{ position: \"absolute\", inset: 0, pointerEvents: \"none\" }}\n        aria-hidden\n      >\n        <filter id={`${uid}-grain`}>\n          <feTurbulence\n            type=\"fractalNoise\"\n            baseFrequency={0.85}\n            numOctaves={2}\n            seed={11}\n          />\n          <feColorMatrix\n            type=\"matrix\"\n            values=\"0 0 0 0 0.5  0 0 0 0 0.5  0 0 0 0 0.5  0 0 0 1 0\"\n          />\n        </filter>\n        <rect\n          width={W}\n          height={H}\n          filter={`url(#${uid}-grain)`}\n          opacity={isLight ? 0.05 : 0.06}\n          style={{ mixBlendMode: isLight ? \"multiply\" : \"screen\" }}\n        />\n      </svg>\n    </AbsoluteFill>\n  );\n}\n\n/** Frames until the name has landed and the cluster settled. */\nexport const featureChipsFlowLength = HIT + 24;\n",
      "type": "registry:component",
      "target": "components/remocn/feature-chips-flow.tsx"
    }
  ],
  "type": "registry:component"
}
