{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "follower-rush",
  "title": "Follower Rush",
  "description": "An X-style follower notification that piles up — avatars stack in and the count explodes, then the row bends into an undulating wave of faces.",
  "dependencies": [
    "remotion",
    "@remotion/google-fonts"
  ],
  "registryDependencies": [
    "https://snapcn.dev/r/snap-cn-ui.json"
  ],
  "files": [
    {
      "path": "registry/snap-cn/follower-rush/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadSans } from \"@remotion/google-fonts/Inter\";\nimport { useState } from \"react\";\nimport {\n  AbsoluteFill,\n  getRemotionEnvironment,\n  Img,\n  interpolate,\n  staticFile,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\nimport { type SnapCnTheme, useSnapCnTheme } from \"@/lib/snap-cn-ui\";\n\nconst { fontFamily: FONT_FAMILY } = loadSans(\"normal\", {\n  weights: [\"400\", \"500\", \"700\", \"800\"],\n  subsets: [\"latin\"],\n});\n\nexport interface Follower {\n  name: string;\n  /**\n   * Square photo for this follower. Root-relative paths (`/avatars/ada.jpg`) are\n   * served by the app in the browser and rewritten through `staticFile()` in a\n   * render; absolute URLs are passed straight through.\n   *\n   * Omit it and the avatar falls back to the gradient monogram, which is what\n   * makes the fallback worth having: a crowd is the one place a missing photo\n   * shows up as a hole in the row.\n   */\n  avatar?: string;\n}\n\nexport interface FollowerRushProps {\n  totalFollowers?: number;\n  followers?: Follower[];\n  /** Overrides the design system's `primary`. */\n  accentColor?: string;\n  /** Design-system token overrides. */\n  theme?: Partial<SnapCnTheme>;\n  mode?: \"light\" | \"dark\";\n  orientation?: \"horizontal\" | \"vertical\";\n  speed?: number;\n}\n\ninterface Theme {\n  bg: string;\n  fg: string;\n  fgMuted: string;\n  /** Placeholder disc behind a photo — `muted`, one step off the page. */\n  avatarBg: string;\n}\n\n// The four surfaces this scene paints, taken from the design system rather\n// than mirrored as hex — so a user's own token overrides reach the pile-up\n// instead of stopping at a copy of the defaults.\nfunction paletteFrom(t: SnapCnTheme): Theme {\n  return {\n    bg: t.card,\n    fg: t.foreground,\n    fgMuted: t.mutedForeground,\n    avatarBg: t.muted,\n  };\n}\n\n/**\n * How many photos the sample crowd cycles through.\n *\n * 24, because the wave holds 22 avatars and scrolls two spare slots past the\n * edge (`MAX + 2`), so 24 is the smallest set where no two faces are ever on\n * screen together. Fewer is fine — they just repeat sooner. More is wasted; only\n * 24 can ever be visible at once.\n */\nconst SAMPLE_AVATAR_COUNT = 24;\n\n/**\n * The crowd of names shown in the pile and named in the callout. Purely\n * flavour — swap it via the `followers` prop.\n *\n * Photos come from `public/avatars/01.jpg … 24.jpg`, kept local so the scene\n * still renders with no network and no CORS. **The files are the only thing this\n * expects — nothing here needs editing to add them.** Any that are missing fall\n * back to the gradient monogram rather than failing the render, which is also\n * what makes this list safe to ship before the folder is filled.\n *\n * There are more names than photos on purpose: the names cycle faster than the\n * faces, so a repeat of either never lines up with a repeat of the other.\n */\nexport const SAMPLE_FOLLOWERS: Follower[] = [\n  \"Manon\",\n  \"Melon\",\n  \"Victor\",\n  \"Shane\",\n  \"Lisa\",\n  \"Natasha\",\n  \"Annie\",\n  \"Abdull\",\n  \"Kratos\",\n  \"Jhone\",\n  \"Matt\",\n  \"Huggy\",\n  \"Felomi\",\n  \"Hazar\",\n  \"Mikasa\",\n  \"Silmon\",\n  \"Luciano\",\n  \"Nova\",\n  \"Priya\",\n  \"Theo\",\n  \"Amelia\",\n  \"Rafael\",\n  \"Sofia\",\n  \"Kai\",\n  \"Jordan\",\n  \"Nora\",\n  \"Dana\",\n  \"Milo\",\n  \"Yuki\",\n  \"Bruno\",\n  \"Elena\",\n  \"Omar\",\n  \"Ivy\",\n  \"Leo\",\n  \"Zara\",\n  \"Finn\",\n  \"Maya\",\n  \"Cole\",\n].map((name, i) => ({\n  name,\n  avatar: `/avatars/${String((i % SAMPLE_AVATAR_COUNT) + 1).padStart(2, \"0\")}.jpg`,\n}));\n\n// --- Pure helpers (unit-tested) -------------------------------------------\n\nexport function clamp01(x: number): number {\n  return Math.max(0, Math.min(1, x));\n}\n\nexport function smoothstep(x: number): number {\n  const c = clamp01(x);\n  return c * c * (3 - 2 * c);\n}\n\nconst lerp = (a: number, b: number, t: number) => a + (b - a) * t;\n\n/**\n * The running follower total at effective frame `fc`. Holds at 1 through the\n * inline intro; then grows *linearly* to a full pile (`midCount`) by `midF`;\n * then *explodes exponentially* to `target`, landing on it at `endF`. This is\n * the reference's shape — a believable trickle, then a blow-up. Rounded and\n * clamped to `[1, target]`.\n */\nexport function followerCount(\n  fc: number,\n  target: number,\n  startF: number,\n  midF: number,\n  endF: number,\n  midCount: number,\n): number {\n  if (fc <= startF) return 1;\n  if (fc <= midF) {\n    const p = (fc - startF) / (midF - startF);\n    return Math.min(target, Math.max(1, Math.round(1 + p * (midCount - 1))));\n  }\n  const p = clamp01((fc - midF) / (endF - midF));\n  const c = midCount * (target / midCount) ** p;\n  return Math.min(target, Math.round(c));\n}\n\n/**\n * How far the crowd has scrolled, in stage px, at effective frame `fc`.\n *\n * The counter explodes from a full pile to the target between `growEnd` and\n * `explodeEnd`, so the crowd accelerates across exactly that window and then\n * holds. It used to travel at a flat 0.85px/frame, which is one avatar slot for\n * the whole blow-up — the number ran to five thousand while a single face went\n * past, and the wave read as still.\n *\n * Velocity ramps linearly `v0 → vMax` over the window, so position is its\n * integral: quadratic while it accelerates, linear once it holds. Velocity is\n * continuous at the join, which is what stops the hand-off being visible — a\n * speed that steps is a jolt no easing curve can hide afterwards.\n */\nexport function waveScroll(\n  fc: number,\n  growEnd: number,\n  explodeEnd: number,\n  v0: number,\n  vMax: number,\n): number {\n  const f = Math.max(0, fc - growEnd);\n  const T = explodeEnd - growEnd;\n  if (T <= 0) return f * vMax;\n  if (f <= T) return v0 * f + ((vMax - v0) * f * f) / (2 * T);\n  // Distance banked over the ramp, then flat out.\n  return (T * (v0 + vMax)) / 2 + vMax * (f - T);\n}\n\n// --- Sub-components --------------------------------------------------------\n\n/** The X verified seal — the shape is the reference's, the fill is the theme\n *  accent (design-system rule: take the shape, leave the brand's paint). */\nfunction VerifiedBadge({ accent, size }: { accent: string; size: number }) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 22 22\"\n      width={size}\n      height={size}\n      fill={accent}\n      style={{ flexShrink: 0, display: \"block\" }}\n    >\n      <title>Verified</title>\n      <path d=\"M20.396 11c-.018-.646-.215-1.275-.57-1.816-.354-.54-.852-.972-1.438-1.246.223-.607.27-1.264.14-1.897-.131-.634-.437-1.218-.882-1.687-.47-.445-1.053-.75-1.687-.882-.633-.13-1.29-.083-1.897.14-.273-.587-.704-1.086-1.245-1.44S11.647 1.62 11 1.604c-.646.017-1.273.213-1.813.568s-.969.854-1.24 1.44c-.608-.223-1.267-.272-1.902-.14-.635.13-1.22.436-1.69.882-.445.47-.749 1.055-.878 1.688-.13.633-.08 1.29.144 1.896-.587.274-1.087.705-1.443 1.245-.356.54-.555 1.17-.574 1.817.02.647.218 1.276.574 1.817.356.54.856.972 1.443 1.245-.224.606-.274 1.263-.144 1.896.13.634.433 1.218.877 1.688.47.443 1.054.747 1.687.878.633.132 1.29.084 1.897-.136.274.586.705 1.084 1.246 1.439.54.354 1.17.551 1.816.569.647-.016 1.276-.213 1.817-.567s.972-.854 1.245-1.44c.604.239 1.266.296 1.903.164.636-.132 1.22-.447 1.68-.907.46-.46.776-1.044.908-1.681s.075-1.299-.165-1.903c.586-.274 1.084-.705 1.439-1.246.354-.54.551-1.17.569-1.816zM9.662 14.85l-3.429-3.428 1.293-1.302 2.072 2.072 4.4-4.794 1.347 1.246z\" />\n    </svg>\n  );\n}\n\n/** The \"new follower\" silhouette that leads the pile before the wave takes over. */\nfunction PersonIcon({ color, size }: { color: string; size: number }) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 24 24\"\n      width={size}\n      height={size}\n      fill={color}\n      style={{ display: \"block\" }}\n    >\n      <title>New follower</title>\n      <circle cx=\"12\" cy=\"7.2\" r=\"4\" />\n      <path d=\"M12 13.4c-4.05 0-7.2 2.3-7.2 5.6 0 .55.45 1 1 1h12.4c.55 0 1-.45 1-1 0-3.3-3.15-5.6-7.2-5.6z\" />\n    </svg>\n  );\n}\n\n/** Rewrite root-relative assets through staticFile only while rendering. */\nfunction resolveSrc(src: string): string {\n  const isLocal = src.startsWith(\"/\") && !src.startsWith(\"//\");\n  if (isLocal && getRemotionEnvironment().isRendering) {\n    return staticFile(src.replace(/^\\/+/, \"\"));\n  }\n  return src;\n}\n\n/**\n * A follower's photo, over a neutral monogram disc for when there isn't one.\n *\n * The disc used to be a hue derived from the name, which gave the crowd a row of\n * saturated greens and magentas — a palette invented right here, which the\n * design-system rule exists to forbid, and which fought every real photograph\n * put next to it. It is `muted` now: one step off the page, no hue of its own.\n *\n * It stays *underneath* the photo rather than being swapped out for it, so a\n * photo still decoding shows a filled disc instead of a hole in the row.\n * Remotion's `<Img>` holds a render back until it has loaded so a rendered frame\n * never catches that state, but the live `<Player>` and the customizer have no\n * such guarantee, and a row of empty circles reads as broken.\n */\nfunction Avatar({\n  follower,\n  size,\n  ring,\n  theme,\n}: {\n  follower: Follower;\n  size: number;\n  ring: number;\n  theme: Theme;\n}) {\n  const [failedSrc, setFailedSrc] = useState<string | null>(null);\n  return (\n    <div\n      style={{\n        position: \"relative\",\n        width: size,\n        height: size,\n        borderRadius: 9999,\n        background: theme.avatarBg,\n        // Ring in the page colour, drawn as box-shadow so it doesn't grow the\n        // layout box — the overlap pitch stays exact.\n        boxShadow: `0 0 0 ${ring}px ${theme.bg}`,\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        color: theme.fgMuted,\n        fontFamily: FONT_FAMILY,\n        fontWeight: 700,\n        fontSize: size * 0.4,\n        overflow: \"hidden\",\n        flexShrink: 0,\n      }}\n    >\n      {follower.name.charAt(0).toUpperCase()}\n      {follower.avatar && failedSrc !== follower.avatar && (\n        <Img\n          src={resolveSrc(follower.avatar)}\n          // Without a handler Remotion treats a 404 as fatal and kills the whole\n          // render. One follower with a dead photo URL is not a reason to lose the\n          // video — drop back to the monogram already painted underneath.\n          //\n          // Keyed by src, not a bare boolean: a slot keeps its React instance\n          // while the crowd scrolls a *different* follower through it, so a plain\n          // `failed` flag would condemn every later face to the same slot.\n          onError={() => setFailedSrc(follower.avatar ?? null)}\n          // `cover` on a square box: a portrait crops to its centre rather than\n          // letterboxing, which is the one thing a circular avatar cannot do.\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            width: \"100%\",\n            height: \"100%\",\n            objectFit: \"cover\",\n            borderRadius: 9999,\n            // Preflight's `img { max-width: 100% }` is harmless here because the\n            // box is explicitly sized, but say it anyway — this component ships\n            // into other people's stylesheets.\n            maxWidth: \"none\",\n          }}\n        />\n      )}\n    </div>\n  );\n}\n\n/** The bold-name + badge + \"…followed you\" callout under the pile. */\nfunction FollowLine({\n  name,\n  others,\n  fontSize,\n  theme,\n  accent,\n}: {\n  name: string;\n  others: number;\n  fontSize: number;\n  theme: Theme;\n  accent: string;\n}) {\n  return (\n    <div\n      style={{\n        display: \"inline-flex\",\n        alignItems: \"center\",\n        gap: fontSize * 0.24,\n        whiteSpace: \"nowrap\",\n        fontFamily: FONT_FAMILY,\n        fontSize,\n        lineHeight: 1,\n      }}\n    >\n      <span\n        style={{ fontWeight: 800, color: theme.fg, letterSpacing: \"-0.01em\" }}\n      >\n        {name}\n      </span>\n      <VerifiedBadge accent={accent} size={fontSize * 0.62} />\n      <span style={{ fontWeight: 500, color: theme.fgMuted }}>\n        {others <= 0\n          ? \"followed you\"\n          : `and ${others.toLocaleString(\"en-US\")} others followed you`}\n      </span>\n    </div>\n  );\n}\n\n// --- Main composition ------------------------------------------------------\n\nexport function FollowerRush({\n  totalFollowers = 5000,\n  followers = SAMPLE_FOLLOWERS,\n  accentColor,\n  theme,\n  mode,\n  orientation = \"horizontal\",\n  speed = 1,\n}: FollowerRushProps) {\n  const frame = useCurrentFrame();\n  const { width, height } = useVideoConfig();\n  const tokens = useSnapCnTheme(theme, mode);\n  const t = paletteFrom(tokens);\n  const accent = accentColor ?? tokens.primary;\n  const pool = followers.length > 0 ? followers : SAMPLE_FOLLOWERS;\n  const isVertical = orientation === \"vertical\";\n\n  const refW = isVertical ? 720 : 1280;\n  const refH = isVertical ? 1280 : 720;\n  const stageScale = Math.min(width / refW, height / refH);\n  const fc = frame * speed;\n\n  // --- timeline (effective frames, fps 30) ---\n  const APPEAR = 8;\n  const INLINE_END = 20; // single \"X followed you\" notification holds until here\n  const MORPH_END = 34; // …then lifts into the stacked pile\n  // The pile used to spend 116 frames (3.9s) laying down 21 avatars and the\n  // count did not start climbing in earnest until frame 150 — half the clip gone\n  // before anything felt urgent, which is what read as the numbers being slow.\n  // They were not slower; they started later. 50 frames to fill, and the blow-up\n  // begins at 1.8s instead of 5s.\n  const GROW_END = 84; // pile fills to MAX; wave + scroll begin\n  const EXPLODE_END = 140; // count lands on the target; row has fully bent to a wave\n\n  const MAX = 22; // most avatars ever shown at once\n\n  // `totalFollowers` is the headline \"others\" number, so the running total that\n  // drives the pile is one more (the lead + the others).\n  const others = Math.max(0, Math.round(totalFollowers));\n  const target = others + 1;\n  const count = followerCount(\n    fc,\n    target,\n    INLINE_END,\n    GROW_END,\n    EXPLODE_END,\n    MAX,\n  );\n  const shownOthers = count - 1;\n\n  // --- layout constants (reference stage px) ---\n  const D = isVertical ? 60 : 66; // avatar diameter\n  const ring = isVertical ? 3 : 4;\n  const pilePitch = D * 0.76; // overlap in the flat pile\n  const iconSize = D * 0.92;\n  const iconGap = D * 0.24;\n  const rowY = isVertical ? refH * 0.4 : 316; // avatar row centre (stacked)\n  const textY = isVertical ? refH * 0.52 : 452; // follow-line centre (stacked)\n  const inlineY = refH / 2;\n  const fontSize = isVertical ? 40 : 48;\n\n  // --- phase progresses ---\n  const globalFade = interpolate(fc, [0, APPEAR], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n  const stackP = smoothstep((fc - INLINE_END) / (MORPH_END - INLINE_END));\n  // The inline notification clears first; the stacked callout appears after, so\n  // the two \"…followed you\" lines never overlap during the morph.\n  const inlineOut = smoothstep((fc - INLINE_END) / 7);\n  const textIn = smoothstep((fc - (INLINE_END + 7)) / (MORPH_END - INLINE_END));\n  const sp = smoothstep((fc - GROW_END) / (EXPLODE_END - GROW_END)); // flat→wave\n  const iconOpacity = interpolate(fc, [GROW_END - 6, GROW_END + 36], [1, 0], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n\n  // Lead (bold) name cycles ~2.3×/s while followers rush in, then freezes on the\n  // last one so the held wave doesn't flicker names forever.\n  const NAME_SLOT = 9;\n  const nameFreeze = Math.floor((EXPLODE_END - INLINE_END) / NAME_SLOT);\n  const nameIdx =\n    fc < INLINE_END\n      ? 0\n      : Math.min(Math.floor((fc - INLINE_END) / NAME_SLOT), nameFreeze);\n  const leadName = pool[nameIdx % pool.length].name;\n\n  // --- wave / pile geometry ---\n  const waveMargin = isVertical ? 44 : 74;\n  const waveLeft = waveMargin;\n  const waveSpan = refW - waveMargin * 2;\n  const waveP = waveSpan / (MAX - 1);\n  const waveAmp = (isVertical ? 40 : 46) * sp;\n  const WAVE_FREQ = 1.55; // periods across the strip\n  const wavePhase = fc * 0.05; // the wave travels\n  // Left alone deliberately: the crowd rushing *through* a slow-moving sine is\n  // what makes them surf it. Speed the field up with them and the two motions\n  // cancel — the avatars would slide sideways along a wave that no longer\n  // appears to lift them.\n  //\n  // ~22px/frame flat out is one avatar slot every ~2.5 frames (the wave pitch is\n  // ~54px here), so about twelve faces a second stream past at the peak.\n  //\n  // This is near the ceiling for a 30fps composition: at 22px the crowd moves a\n  // third of an avatar's diameter per frame, and much past that the row stops\n  // reading as travelling and starts reading as strobing — discrete copies\n  // rather than motion. Faster than this wants motion blur, not a bigger number.\n  const scrollPx = waveScroll(\n    fc,\n    GROW_END,\n    EXPLODE_END,\n    isVertical ? 0.6 : 0.85,\n    isVertical ? 15 : 22,\n  );\n  const scrollUnit = Math.floor(scrollPx / waveP);\n\n  // Flat pile: icon + avatars, centred as one group. Width grows smoothly with\n  // the (un-rounded) count so adding an avatar doesn't jolt the centring.\n  const pileCountF = Math.min(\n    MAX,\n    fc <= INLINE_END\n      ? 1\n      : fc <= GROW_END\n        ? 1 + ((fc - INLINE_END) / (GROW_END - INLINE_END)) * (MAX - 1)\n        : MAX,\n  );\n  const pileW = (pileCountF - 1) * pilePitch + D;\n  const groupW = iconSize + iconGap + pileW;\n  const groupLeft = (refW - groupW) / 2;\n  const pileStartCX = groupLeft + iconSize + iconGap + D / 2;\n  const iconCX = groupLeft + iconSize / 2;\n\n  const isRendering = getRemotionEnvironment().isRendering;\n  const willChange = isRendering ? undefined : (\"transform\" as const);\n\n  const SLOTS = MAX + 2; // two extra so the scroll never opens an edge gap\n\n  return (\n    <AbsoluteFill style={{ background: t.bg }}>\n      <div\n        style={{\n          position: \"absolute\",\n          left: \"50%\",\n          top: \"50%\",\n          width: refW,\n          height: refH,\n          transform: `translate(-50%, -50%) scale(${stageScale})`,\n          opacity: globalFade,\n        }}\n      >\n        {/* ---- Inline first notification: [icon][avatar] Name ✓ followed you.\n             Fades up and out as the pile takes over. */}\n        {inlineOut < 1 && (\n          <div\n            style={{\n              position: \"absolute\",\n              inset: 0,\n              display: \"flex\",\n              alignItems: \"center\",\n              justifyContent: \"center\",\n              gap: iconGap,\n              opacity: 1 - inlineOut,\n              transform: `translateY(${lerp(0, -18, inlineOut)}px)`,\n              willChange,\n            }}\n          >\n            <PersonIcon color={accent} size={iconSize} />\n            <Avatar follower={pool[0]} size={D} ring={ring} theme={t} />\n            <div style={{ marginLeft: iconGap * 0.6 }}>\n              <FollowLine\n                name={pool[0].name}\n                others={0}\n                fontSize={fontSize}\n                theme={t}\n                accent={accent}\n              />\n            </div>\n          </div>\n        )}\n\n        {/* ---- Person icon leading the flat pile (fades out as the wave forms) */}\n        {iconOpacity > 0 && sp < 1 && (\n          <div\n            style={{\n              position: \"absolute\",\n              left: iconCX,\n              top: lerp(inlineY, rowY, stackP),\n              transform: `translate(-50%, -50%) scale(${1 - sp})`,\n              opacity: iconOpacity * stackP,\n            }}\n          >\n            <PersonIcon color={accent} size={iconSize} />\n          </div>\n        )}\n\n        {/* ---- The avatar crowd: a flat overlapping pile that bends into a\n             travelling wave. A single edge mask fades the wave's ends; the\n             centred pile never reaches the edges, so it is untouched. */}\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            opacity: stackP,\n            WebkitMaskImage: `linear-gradient(to right, transparent 0%, #000 ${(waveMargin / refW) * 100 + 1}%, #000 ${100 - (waveMargin / refW) * 100 - 1}%, transparent 100%)`,\n            maskImage: `linear-gradient(to right, transparent 0%, #000 ${(waveMargin / refW) * 100 + 1}%, #000 ${100 - (waveMargin / refW) * 100 - 1}%, transparent 100%)`,\n          }}\n        >\n          {Array.from({ length: SLOTS }, (_, i) => {\n            const follower = pool[(i + scrollUnit) % pool.length];\n            // pile position (flat, centred) → wave position (full width, sine)\n            const pileCX = pileStartCX + i * pilePitch;\n            const waveCX = waveLeft + i * waveP - (scrollPx % waveP);\n            const cx = lerp(pileCX, waveCX, sp);\n            const pileCY = lerp(inlineY, rowY, stackP);\n            const waveCY =\n              rowY +\n              waveAmp *\n                Math.sin((cx / refW) * Math.PI * 2 * WAVE_FREQ + wavePhase);\n            const cy = lerp(pileCY, waveCY, sp);\n\n            // pile avatars pop in as the count reaches them; the two extra\n            // scroll slots only exist once the wave is spread out.\n            const popIn = smoothstep(clamp01(count - i));\n            const baseOpacity = i < MAX ? popIn : 0;\n            const opacity = lerp(baseOpacity, 1, sp);\n            if (opacity <= 0.001) return null;\n            const popScale = lerp(0.55, 1, popIn);\n\n            return (\n              <div\n                // biome-ignore lint/suspicious/noArrayIndexKey: slots are positional; the follower shown in a slot changes as the crowd scrolls, so keying by follower would remount every frame.\n                key={i}\n                style={{\n                  position: \"absolute\",\n                  left: cx,\n                  top: cy,\n                  transform: `translate(-50%, -50%) scale(${lerp(popScale, 1, sp)})`,\n                  opacity,\n                  zIndex: SLOTS - i, // leftmost on top\n                  willChange,\n                }}\n              >\n                <Avatar follower={follower} size={D} ring={ring} theme={t} />\n              </div>\n            );\n          })}\n        </div>\n\n        {/* ---- Follow-line callout (stacked), centred under the pile/wave. */}\n        <div\n          style={{\n            position: \"absolute\",\n            left: 0,\n            right: 0,\n            top: textY,\n            display: \"flex\",\n            justifyContent: \"center\",\n            transform: `translateY(-50%) translateY(${lerp(12, 0, textIn)}px)`,\n            opacity: textIn,\n            textRendering: \"geometricPrecision\",\n            willChange,\n          }}\n        >\n          <FollowLine\n            name={leadName}\n            others={shownOthers}\n            fontSize={fontSize}\n            theme={t}\n            accent={accent}\n          />\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/snap-cn/follower-rush.tsx"
    }
  ],
  "type": "registry:component"
}