{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "github-sponsors",
  "title": "GitHub Sponsors",
  "description": "Self-drawing Sponsors heart that docks to a header, every sponsor avatar blur-staggering into a grid, then a thank-you headline and a CTA. Single seek-safe timeline, 270 frames at 30fps.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "registryDependencies": ["@remocn/soft-blur-in", "@remocn/blur-out-up"],
  "files": [
    {
      "path": "registry/remocn/github-sponsors/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadSans } from \"@remotion/google-fonts/Manrope\";\nimport { memo, useMemo, useState } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  Img,\n  interpolate,\n  Sequence,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\nimport { BlurOutUp } from \"@/components/remocn/blur-out-up\";\nimport { SoftBlurIn } from \"@/components/remocn/soft-blur-in\";\n\nexport interface Sponsor {\n  login: string;\n  avatarUrl: string;\n}\n\nexport interface GitHubSponsorsProps {\n  account?: string;\n  sponsors?: Sponsor[];\n  accentColor?: string;\n  speed?: number;\n  theme?: \"light\" | \"dark\";\n}\n\nexport const SAMPLE_SPONSORS: Sponsor[] = [\n  {\n    login: \"mojombo\",\n    avatarUrl: \"https://avatars.githubusercontent.com/u/1?v=4\",\n  },\n  {\n    login: \"defunkt\",\n    avatarUrl: \"https://avatars.githubusercontent.com/u/2?v=4\",\n  },\n  {\n    login: \"pjhyett\",\n    avatarUrl: \"https://avatars.githubusercontent.com/u/3?v=4\",\n  },\n  {\n    login: \"wycats\",\n    avatarUrl: \"https://avatars.githubusercontent.com/u/4?v=4\",\n  },\n  {\n    login: \"schacon\",\n    avatarUrl: \"https://avatars.githubusercontent.com/u/70?v=4\",\n  },\n  {\n    login: \"rtomayko\",\n    avatarUrl: \"https://avatars.githubusercontent.com/u/5?v=4\",\n  },\n  {\n    login: \"bmizerany\",\n    avatarUrl: \"https://avatars.githubusercontent.com/u/6?v=4\",\n  },\n  {\n    login: \"atmos\",\n    avatarUrl: \"https://avatars.githubusercontent.com/u/38?v=4\",\n  },\n  {\n    login: \"ezmobius\",\n    avatarUrl: \"https://avatars.githubusercontent.com/u/14?v=4\",\n  },\n  {\n    login: \"macournoyer\",\n    avatarUrl: \"https://avatars.githubusercontent.com/u/15?v=4\",\n  },\n  {\n    login: \"ivey\",\n    avatarUrl: \"https://avatars.githubusercontent.com/u/18?v=4\",\n  },\n  {\n    login: \"uggedal\",\n    avatarUrl: \"https://avatars.githubusercontent.com/u/16?v=4\",\n  },\n  {\n    login: \"anotherjesse\",\n    avatarUrl: \"https://avatars.githubusercontent.com/u/9?v=4\",\n  },\n  {\n    login: \"technoweenie\",\n    avatarUrl: \"https://avatars.githubusercontent.com/u/21?v=4\",\n  },\n  {\n    login: \"takeo\",\n    avatarUrl: \"https://avatars.githubusercontent.com/u/22?v=4\",\n  },\n  {\n    login: \"evanphx\",\n    avatarUrl: \"https://avatars.githubusercontent.com/u/25?v=4\",\n  },\n  {\n    login: \"vanpelt\",\n    avatarUrl: \"https://avatars.githubusercontent.com/u/26?v=4\",\n  },\n  {\n    login: \"wayneeseguin\",\n    avatarUrl: \"https://avatars.githubusercontent.com/u/28?v=4\",\n  },\n  {\n    login: \"brynary\",\n    avatarUrl: \"https://avatars.githubusercontent.com/u/30?v=4\",\n  },\n  {\n    login: \"kevinclark\",\n    avatarUrl: \"https://avatars.githubusercontent.com/u/31?v=4\",\n  },\n  {\n    login: \"kballard\",\n    avatarUrl: \"https://avatars.githubusercontent.com/u/7?v=4\",\n  },\n  {\n    login: \"jnewland\",\n    avatarUrl: \"https://avatars.githubusercontent.com/u/12?v=4\",\n  },\n  {\n    login: \"caged\",\n    avatarUrl: \"https://avatars.githubusercontent.com/u/13?v=4\",\n  },\n  { login: \"sr\", avatarUrl: \"https://avatars.githubusercontent.com/u/19?v=4\" },\n];\n\nconst { fontFamily: SANS_FAMILY } = loadSans();\nconst FONT_FAMILY = SANS_FAMILY;\n\nconst HEART_PATH =\n  \"M12.0001 4.52853C14.2745 2.23823 17.9866 2.23823 20.2611 4.52853C22.5784 6.86156 22.5784 10.667 20.2611 13.0001L12.0001 21.3199L3.73907 13.0001C1.42175 10.667 1.42175 6.86156 3.73907 4.52853C6.01353 2.23823 9.72563 2.23823 12.0001 4.52853Z\";\n\nconst REF_W = 1280;\nconst REF_H = 720;\nconst GRID_CX = 640;\nconst GRID_CY = 304;\nconst GRID_BOX_W = 900;\nconst GRID_BOX_H = 236;\nconst HEART_START_Y = 286;\nconst HEART_HEADER_Y = 120;\n\nconst DRAW_END = 40;\nconst PULSE_END = 62;\nconst HEART_DOCK_START = 52;\nconst HEART_DOCK_END = 84;\nconst AVATAR_START = 70;\nconst AVATAR_STEP = 3;\nconst AVATAR_DUR = 22;\nconst AVATAR_WINDOW = 56;\nconst THANK_START = 150;\nconst SUB_START = 166;\nconst CTA_START = 182;\nconst CTA_DUR = 22;\n\ninterface Theme {\n  bg: string;\n  bgSubtle: string;\n  fg: string;\n  fgMuted: string;\n  border: string;\n}\n\nconst THEMES: Record<\"light\" | \"dark\", Theme> = {\n  light: {\n    bg: \"#ffffff\",\n    bgSubtle: \"#fafafa\",\n    fg: \"#171717\",\n    fgMuted: \"#737373\",\n    border: \"#ededed\",\n  },\n  dark: {\n    bg: \"#0a0a0a\",\n    bgSubtle: \"#161618\",\n    fg: \"#fafafa\",\n    fgMuted: \"#a1a1aa\",\n    border: \"#262626\",\n  },\n};\n\nexport interface Point {\n  x: number;\n  y: number;\n}\n\nexport function gridColumns(\n  count: number,\n  boxW = GRID_BOX_W,\n  boxH = GRID_BOX_H,\n): number {\n  if (count <= 0) return 0;\n  return Math.max(\n    1,\n    Math.min(count, Math.round(Math.sqrt((count * boxW) / boxH))),\n  );\n}\n\nexport function gridLayout(\n  count: number,\n  cols: number,\n  cellW: number,\n  cellH: number,\n  cx = GRID_CX,\n  cy = GRID_CY,\n): Point[] {\n  if (count <= 0 || cols <= 0) return [];\n  const rows = Math.ceil(count / cols);\n  const out: Point[] = [];\n  for (let i = 0; i < count; i++) {\n    const r = Math.floor(i / cols);\n    const c = i % cols;\n    const rowCount = Math.min(cols, count - r * cols);\n    const rowW = (rowCount - 1) * cellW;\n    const x = cx - rowW / 2 + c * cellW;\n    const totalH = (rows - 1) * cellH;\n    const y = cy - totalH / 2 + r * cellH;\n    out.push({ x, y });\n  }\n  return out;\n}\n\nexport function heartDrawProgress(frame: number, speed = 1): number {\n  return interpolate(frame * speed, [0, DRAW_END], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: Easing.bezier(0.22, 1, 0.36, 1),\n  });\n}\n\nexport function heartPulseScale(frame: number, speed = 1): number {\n  return interpolate(\n    frame * speed,\n    [DRAW_END - 2, DRAW_END + 6, DRAW_END + 17, PULSE_END],\n    [1, 0.9, 1.06, 1],\n    {\n      extrapolateLeft: \"clamp\",\n      extrapolateRight: \"clamp\",\n      easing: Easing.inOut(Easing.ease),\n    },\n  );\n}\n\nfunction ramp(\n  frame: number,\n  speed: number,\n  start: number,\n  end: number,\n  easing = Easing.bezier(0.22, 1, 0.36, 1),\n): number {\n  return interpolate(frame * speed, [start, end], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing,\n  });\n}\n\nfunction Avatar({\n  login,\n  avatarUrl,\n  size,\n  theme,\n  grayscale,\n}: {\n  login: string;\n  avatarUrl: string;\n  size: number;\n  theme: Theme;\n  grayscale: number;\n}) {\n  const [errored, setErrored] = useState(false);\n  const base = {\n    width: size,\n    height: size,\n    borderRadius: 9999,\n    border: `1px solid ${theme.border}`,\n    flexShrink: 0,\n    filter: `grayscale(${grayscale})`,\n  } as const;\n\n  if (errored || !avatarUrl) {\n    return (\n      <div\n        style={{\n          ...base,\n          background: theme.bgSubtle,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          color: theme.fgMuted,\n          fontSize: size * 0.42,\n          fontWeight: 600,\n          fontFamily: FONT_FAMILY,\n        }}\n      >\n        {login.charAt(0).toUpperCase()}\n      </div>\n    );\n  }\n\n  return (\n    <Img\n      src={avatarUrl}\n      crossOrigin=\"anonymous\"\n      onError={() => setErrored(true)}\n      style={{ ...base, objectFit: \"cover\" }}\n    />\n  );\n}\n\nconst SponsorNode = memo(function SponsorNode({\n  sponsor,\n  cell,\n  progress,\n  size,\n  theme,\n}: {\n  sponsor: Sponsor;\n  cell: Point;\n  progress: number;\n  size: number;\n  theme: Theme;\n}) {\n  const y = (1 - progress) * 14;\n  const blur = (1 - progress) * 10;\n\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        left: cell.x,\n        top: cell.y,\n        transform: `translate(-50%, -50%) translateY(${y}px)`,\n        opacity: progress,\n        filter: `blur(${blur}px)`,\n        willChange: \"transform, opacity, filter\",\n      }}\n    >\n      <Avatar\n        login={sponsor.login}\n        avatarUrl={sponsor.avatarUrl}\n        size={size}\n        theme={theme}\n        grayscale={1 - progress}\n      />\n    </div>\n  );\n});\n\nfunction HeartGlyph({\n  size,\n  accent,\n  draw,\n  fill,\n}: {\n  size: number;\n  accent: string;\n  draw: number;\n  fill: number;\n}) {\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    >\n      <title>GitHub Sponsors</title>\n      <path\n        d={HEART_PATH}\n        pathLength={1}\n        fill={accent}\n        fillOpacity={fill}\n        stroke={accent}\n        strokeWidth={1.4}\n        strokeLinejoin=\"round\"\n        strokeLinecap=\"round\"\n        strokeDasharray={1}\n        strokeDashoffset={1 - draw}\n      />\n    </svg>\n  );\n}\n\nexport function GitHubSponsors({\n  account = \"remocn\",\n  sponsors = SAMPLE_SPONSORS,\n  accentColor = \"#db61a2\",\n  speed = 1,\n  theme = \"light\",\n}: GitHubSponsorsProps) {\n  const frame = useCurrentFrame();\n  const { width, height } = useVideoConfig();\n  const t = THEMES[theme] ?? THEMES.light;\n  const stageScale = Math.min(width / REF_W, height / REF_H);\n\n  const count = sponsors.length;\n  const avatarStep =\n    count > 1 ? Math.min(AVATAR_STEP, AVATAR_WINDOW / (count - 1)) : 0;\n  const cols = gridColumns(count);\n  const rows = Math.max(1, Math.ceil(count / Math.max(1, cols)));\n  const cellW = GRID_BOX_W / Math.max(1, cols);\n  const cellH = GRID_BOX_H / rows;\n  const avatarSize = Math.min(84, Math.min(cellW, cellH) * 0.76);\n\n  const cells = useMemo(\n    () => gridLayout(count, cols, cellW, cellH),\n    [count, cols, cellW, cellH],\n  );\n\n  const draw = heartDrawProgress(frame, speed);\n  const fillO = ramp(frame, speed, DRAW_END - 8, PULSE_END, Easing.linear);\n  const pulse = heartPulseScale(frame, speed);\n  const dock = ramp(frame, speed, HEART_DOCK_START, HEART_DOCK_END);\n\n  const heartBase = 168;\n  const heartScale = (1 - 0.62 * dock) * pulse;\n  const heartDy = (HEART_HEADER_Y - HEART_START_Y) * dock;\n\n  const ctaProgress = ramp(frame, speed, CTA_START, CTA_START + CTA_DUR);\n  const ctaBlur = (1 - ctaProgress) * 10;\n  const ctaY = (1 - ctaProgress) * 12;\n  const accountOpacity = ramp(\n    frame,\n    speed,\n    CTA_START + 8,\n    CTA_START + CTA_DUR + 8,\n  );\n\n  return (\n    <AbsoluteFill style={{ background: t.bg }}>\n      <div\n        style={{\n          position: \"absolute\",\n          left: \"50%\",\n          top: \"50%\",\n          width: REF_W,\n          height: REF_H,\n          transform: `translate(-50%, -50%) scale(${stageScale})`,\n        }}\n      >\n        {sponsors.map((sponsor, i) => (\n          <SponsorNode\n            key={`node-${sponsor.login}-${i}`}\n            sponsor={sponsor}\n            cell={cells[i]}\n            progress={ramp(\n              frame,\n              speed,\n              AVATAR_START + i * avatarStep,\n              AVATAR_START + i * avatarStep + AVATAR_DUR,\n            )}\n            size={avatarSize}\n            theme={t}\n          />\n        ))}\n\n        <div\n          style={{\n            position: \"absolute\",\n            left: GRID_CX,\n            top: HEART_START_Y,\n            transform: `translate(-50%, -50%) translate(0px, ${heartDy}px) scale(${heartScale})`,\n            width: heartBase,\n            height: heartBase,\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n            willChange: \"transform\",\n          }}\n        >\n          <HeartGlyph\n            size={heartBase}\n            accent={accentColor}\n            draw={draw}\n            fill={fillO}\n          />\n        </div>\n\n        <div\n          style={{\n            position: \"absolute\",\n            left: 0,\n            top: 444,\n            width: REF_W,\n            height: 84,\n          }}\n        >\n          <Sequence from={Math.round(THANK_START / speed)} layout=\"none\">\n            <SoftBlurIn\n              text=\"Thank you\"\n              fontSize={54}\n              fontWeight={600}\n              color={t.fg}\n              speed={speed}\n            />\n          </Sequence>\n        </div>\n\n        <div\n          style={{\n            position: \"absolute\",\n            left: 0,\n            top: 532,\n            width: REF_W,\n            height: 36,\n          }}\n        >\n          <Sequence from={Math.round(SUB_START / speed)} layout=\"none\">\n            <BlurOutUp\n              text={`Powered by ${count} sponsors`}\n              fontSize={24}\n              fontWeight={500}\n              color={t.fgMuted}\n              speed={speed}\n            />\n          </Sequence>\n        </div>\n\n        <div\n          style={{\n            position: \"absolute\",\n            left: 0,\n            top: 584,\n            width: REF_W,\n            display: \"flex\",\n            flexDirection: \"column\",\n            alignItems: \"center\",\n            gap: 12,\n          }}\n        >\n          <div\n            style={{\n              transform: `translateY(${ctaY}px)`,\n              opacity: ctaProgress,\n              filter: `blur(${ctaBlur}px)`,\n              display: \"flex\",\n              alignItems: \"center\",\n              gap: 10,\n              padding: \"14px 26px\",\n              borderRadius: 9999,\n              background: accentColor,\n              color: \"#ffffff\",\n              fontSize: 24,\n              fontWeight: 600,\n              fontFamily: FONT_FAMILY,\n              willChange: \"transform, opacity, filter\",\n            }}\n          >\n            <HeartGlyph size={22} accent=\"#ffffff\" draw={1} fill={1} />\n            Become a sponsor\n          </div>\n          <div\n            style={{\n              opacity: accountOpacity * 0.9,\n              fontSize: 18,\n              fontWeight: 400,\n              color: t.fgMuted,\n              fontFamily: FONT_FAMILY,\n            }}\n          >\n            github.com/sponsors/{account}\n          </div>\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/github-sponsors.tsx"
    }
  ],
  "type": "registry:component"
}
