{
  "name": "end-card",
  "type": "registry:block",
  "description": "Outro built in the order a viewer acts on it: title, button assembling under its label, address typing, channels, one pulse",
  "dependencies": [
    "remotion",
    "@remotion/google-fonts"
  ],
  "registryDependencies": [
    "code-syntax",
    "layout",
    "motion-tokens"
  ],
  "files": [
    {
      "path": "registry/bases/default/scenes/end-card/index.tsx",
      "type": "registry:block",
      "content": "import { loadFont } from \"@remotion/google-fonts/Inter\";\nimport { Img, interpolate, spring, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { CODE_THEMES } from \"@/remotion/lib/code-syntax\";\nimport { getSafeAreaPadding } from \"@/remotion/lib/layout\";\nimport { EASING } from \"@/remotion/lib/motion-tokens\";\n\nconst { fontFamily } = loadFont(\"normal\", {\n  weights: [\"400\", \"500\", \"600\", \"700\"],\n  subsets: [\"latin\"],\n});\n\nexport type EndCardProps = {\n  title: string;\n  /** Line under the title. */\n  subtitle?: string;\n  /** Chip above the title. */\n  eyebrow?: string;\n  /** Button label. Omit to end on the title alone. */\n  cta?: string;\n  /** Address typed under the button. */\n  url?: string;\n  /** Handles or channels listed along the foot. */\n  handles?: string[];\n  logoSrc?: string;\n  logoSize?: number;\n  backgroundColor?: string;\n  accentColor?: string;\n  theme?: \"dark\" | \"light\";\n  /** Animation speed multiplier. */\n  speed?: number;\n};\n\n/** Beat plan in seconds. */\nconst T = {\n  logo: 0,\n  eyebrow: 0.14,\n  title: 0.26,\n  subtitle: 0.6,\n  /** The button assembles, then its label rises out of it. */\n  button: 0.82,\n  buttonLabel: 0.98,\n  /** Address types under the button. */\n  url: 1.24,\n  handles: 1.6,\n  /** Attention pulse once everything is standing. */\n  pulse: 2.05,\n} as const;\n\n/** Characters per second the address types at. */\nconst URL_CPS = 26;\n\nconst clamp = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\n/**\n * The outro built in the order a viewer acts on it: the title lands, the button\n * assembles and its label rises out of it, the address types underneath, the\n * channels list, and a single ring pulses off the button — rather than a stack\n * of text fading in together.\n */\nexport const EndCard: React.FC<EndCardProps> = ({\n  title,\n  subtitle,\n  eyebrow,\n  cta,\n  url,\n  handles,\n  logoSrc,\n  logoSize,\n  backgroundColor,\n  accentColor = \"#E8B86D\",\n  theme = \"dark\",\n  speed = 1,\n}) => {\n  const frame = useCurrentFrame();\n  const { fps, width, height } = useVideoConfig();\n  const palette = CODE_THEMES[theme];\n  const safe = getSafeAreaPadding({ width, height });\n\n  const now = (frame / fps) * speed;\n  const at = (seconds: number) => (seconds * fps) / speed;\n  const ease = (from: number, to: number, easing = EASING.enter) =>\n    interpolate(frame, [at(from), at(to)], [0, 1], { easing, ...clamp });\n\n  const stage = {\n    w: width - safe.paddingLeft - safe.paddingRight,\n    h: height - safe.paddingTop - safe.paddingBottom,\n  };\n  const portrait = height > width;\n  const u = portrait\n    ? Math.min(stage.w / 620, stage.h / 1120)\n    : Math.min(stage.w / 1120, stage.h / 620);\n\n  const logoIn = logoSrc ? ease(T.logo, T.logo + 0.5) : 0;\n  const eyebrowIn = eyebrow ? ease(T.eyebrow, T.eyebrow + 0.45) : 0;\n  const titleIn = ease(T.title, T.title + 0.55);\n  const subtitleIn = subtitle ? ease(T.subtitle, T.subtitle + 0.5) : 0;\n  const button = cta\n    ? spring({\n        frame: frame - at(T.button),\n        fps,\n        config: { damping: 15, stiffness: 160, mass: 0.7 },\n      })\n    : 0;\n  const buttonLabel = cta ? ease(T.buttonLabel, T.buttonLabel + 0.4) : 0;\n  const typed = url\n    ? Math.floor(Math.max(now - T.url, 0) * URL_CPS)\n    : 0;\n  const urlDone = url ? typed >= url.length : false;\n  const handlesIn = handles?.length ? ease(T.handles, T.handles + 0.5) : 0;\n  const pulse = cta\n    ? interpolate(frame, [at(T.pulse), at(T.pulse + 0.7)], [0, 1], clamp)\n    : 0;\n\n  return (\n    <div\n      style={{\n        width,\n        height,\n        background: backgroundColor ?? palette.page,\n        fontFamily,\n        position: \"relative\",\n        overflow: \"hidden\",\n        display: \"grid\",\n        placeItems: \"center\",\n      }}\n    >\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          background: `radial-gradient(ellipse 60% 46% at 50% 44%, ${accentColor}1F, transparent 72%)`,\n        }}\n      />\n\n      <div\n        style={{\n          position: \"relative\",\n          width: Math.min(stage.w, 860 * u),\n          display: \"flex\",\n          flexDirection: \"column\",\n          alignItems: \"center\",\n          gap: 16 * u,\n          textAlign: \"center\",\n        }}\n      >\n        {logoSrc ? (\n          <Img\n            src={logoSrc}\n            style={{\n              width: logoSize ?? 76 * u,\n              height: logoSize ?? 76 * u,\n              objectFit: \"contain\",\n              opacity: logoIn,\n              transform: `translateY(${(1 - logoIn) * 12 * u}px)`,\n            }}\n          />\n        ) : null}\n\n        {eyebrow ? (\n          <div\n            style={{\n              color: accentColor,\n              fontSize: 20 * u,\n              fontWeight: 600,\n              letterSpacing: \"0.14em\",\n              textTransform: \"uppercase\",\n              opacity: eyebrowIn,\n              transform: `translateY(${(1 - eyebrowIn) * 10 * u}px)`,\n            }}\n          >\n            {eyebrow}\n          </div>\n        ) : null}\n\n        {/* The title rises out of its own mask */}\n        <div style={{ overflow: \"hidden\", paddingBottom: \"0.04em\" }}>\n          <h2\n            style={{\n              margin: 0,\n              color: palette.fg,\n              fontSize: 68 * u,\n              fontWeight: 700,\n              lineHeight: 1.06,\n              letterSpacing: \"-0.03em\",\n              transform: `translateY(${(1 - titleIn) * 100}%)`,\n            }}\n          >\n            {title}\n          </h2>\n        </div>\n\n        {subtitle ? (\n          <p\n            style={{\n              margin: 0,\n              maxWidth: stage.w * 0.7,\n              color: palette.dim,\n              fontSize: 28 * u,\n              lineHeight: 1.35,\n              opacity: subtitleIn,\n              transform: `translateY(${(1 - subtitleIn) * 12 * u}px)`,\n            }}\n          >\n            {subtitle}\n          </p>\n        ) : null}\n\n        {cta ? (\n          <div style={{ position: \"relative\", marginTop: 10 * u }}>\n            {/* Pulse leaves the button edge, so it reads as a prompt */}\n            <div\n              style={{\n                position: \"absolute\",\n                inset: 0,\n                borderRadius: 999,\n                border: `${2 * u}px solid ${accentColor}`,\n                opacity: pulse > 0 && pulse < 1 ? (1 - pulse) * 0.8 : 0,\n                transform: `scale(${interpolate(pulse, [0, 1], [1, 1.28])})`,\n              }}\n            />\n            <div\n              style={{\n                position: \"relative\",\n                display: \"flex\",\n                alignItems: \"center\",\n                gap: 12 * u,\n                padding: `${17 * u}px ${34 * u}px`,\n                borderRadius: 999,\n                background: accentColor,\n                color: palette.page,\n                fontSize: 27 * u,\n                fontWeight: 700,\n                letterSpacing: \"-0.01em\",\n                boxShadow: `0 ${14 * u}px ${40 * u}px ${accentColor}44`,\n                opacity: Math.min(button, 1),\n                transform: `scale(${interpolate(\n                  Math.min(button, 1),\n                  [0, 1],\n                  [0.9, 1],\n                )})`,\n                overflow: \"hidden\",\n              }}\n            >\n              <span\n                style={{\n                  display: \"block\",\n                  transform: `translateY(${(1 - buttonLabel) * 130}%)`,\n                }}\n              >\n                {cta}\n              </span>\n              <svg\n                width={20 * u}\n                height={20 * u}\n                viewBox=\"0 0 24 24\"\n                fill=\"none\"\n                style={{ opacity: buttonLabel }}\n              >\n                <path\n                  d=\"M5 12h13m0 0-5.4-5.4M18 12l-5.4 5.4\"\n                  stroke={palette.page}\n                  strokeWidth={2.2}\n                  strokeLinecap=\"round\"\n                  strokeLinejoin=\"round\"\n                />\n              </svg>\n            </div>\n          </div>\n        ) : null}\n\n        {url ? (\n          <div\n            style={{\n              marginTop: 4 * u,\n              color: palette.dim,\n              fontSize: 24 * u,\n              fontWeight: 500,\n              display: \"flex\",\n              alignItems: \"center\",\n              opacity: typed > 0 ? 1 : 0,\n            }}\n          >\n            {url.slice(0, typed)}\n            {/* Caret holds while typing, then clears once the address is whole */}\n            <span\n              style={{\n                display: \"inline-block\",\n                width: 2 * u,\n                height: 24 * u,\n                marginLeft: 3 * u,\n                background: accentColor,\n                opacity: urlDone ? 0 : 1,\n              }}\n            />\n          </div>\n        ) : null}\n\n        {handles?.length ? (\n          <div\n            style={{\n              marginTop: 10 * u,\n              display: \"flex\",\n              flexWrap: \"wrap\",\n              justifyContent: \"center\",\n              gap: `${8 * u}px ${20 * u}px`,\n              color: palette.faint,\n              fontSize: 21 * u,\n              opacity: handlesIn,\n              transform: `translateY(${(1 - handlesIn) * 10 * u}px)`,\n            }}\n          >\n            {handles.map((handle) => (\n              <span key={handle}>{handle}</span>\n            ))}\n          </div>\n        ) : null}\n      </div>\n    </div>\n  );\n};\n"
    }
  ],
  "atlas": {
    "lane": "blocks",
    "drive": "time",
    "tier": "core"
  }
}