{
  "name": "auto-fit-title",
  "type": "registry:block",
  "description": "Headline sized to the frame it is given, then assembled word by word so any length lands the same way",
  "dependencies": [
    "remotion",
    "@remotion/google-fonts"
  ],
  "registryDependencies": [
    "code-syntax",
    "layout",
    "motion-tokens",
    "text-fit-utils"
  ],
  "files": [
    {
      "path": "registry/bases/default/scenes/auto-fit-title/index.tsx",
      "type": "registry:block",
      "content": "import { loadFont } from \"@remotion/google-fonts/Inter\";\nimport { Img, interpolate, 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\";\nimport { fitHeadline } from \"@/remotion/lib/text-fit-utils\";\n\nconst { fontFamily } = loadFont(\"normal\", {\n  weights: [\"400\", \"500\", \"600\", \"700\"],\n  subsets: [\"latin\"],\n});\n\nexport type AutoFitTitleProps = {\n  /** Headline. Sized to fill the safe area whatever its length. */\n  title: string;\n  subtitle?: string;\n  logoSrc?: string;\n  logoSize?: number;\n  /** Ceiling for the fitted size, at a 1080-wide stage. */\n  maxFontSize?: number;\n  /** Floor for the fitted size, so a very long title stays legible. */\n  minFontSize?: number;\n  accentColor?: string;\n  backgroundColor?: 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  title: 0.2,\n  /** Added per word. */\n  wordStagger: 0.06,\n  subtitle: 0.72,\n} as const;\n\nconst clamp = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\n/**\n * A headline sized to the frame it is given, then set in motion word by word:\n * the fit is measured before anything animates, so a two-word title and a\n * twelve-word title both fill the safe area and both land the same way.\n */\nexport const AutoFitTitle: React.FC<AutoFitTitleProps> = ({\n  title,\n  subtitle,\n  logoSrc,\n  logoSize,\n  maxFontSize = 128,\n  minFontSize = 34,\n  accentColor = \"#E8B86D\",\n  backgroundColor,\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 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 words = title.split(/\\s+/).filter(Boolean);\n  const fontSize = fitHeadline({\n    text: title,\n    maxWidth: stage.w,\n    maxFontSize: maxFontSize * u,\n    minFontSize: minFontSize * u,\n  });\n\n  const logoIn = logoSrc ? ease(T.logo, T.logo + 0.45) : 0;\n  const subtitleIn = subtitle ? ease(T.subtitle, T.subtitle + 0.5) : 0;\n  /** Slow push, so the card is never dead still on a hold. */\n  const push = ease(0, 7, EASING.editorial);\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 64% 50% at 50% 46%, ${accentColor}1A, transparent 72%)`,\n          transform: `scale(${interpolate(push, [0, 1], [1, 1.1])})`,\n        }}\n      />\n\n      <div\n        style={{\n          position: \"relative\",\n          width: stage.w,\n          display: \"flex\",\n          flexDirection: \"column\",\n          alignItems: \"center\",\n          gap: 20 * u,\n          textAlign: \"center\",\n          transform: `scale(${interpolate(push, [0, 1], [1, 1.02])})`,\n        }}\n      >\n        {logoSrc ? (\n          <Img\n            src={logoSrc}\n            style={{\n              width: logoSize ?? 70 * u,\n              height: logoSize ?? 70 * u,\n              objectFit: \"contain\",\n              opacity: logoIn,\n              transform: `translateY(${(1 - logoIn) * 12 * u}px)`,\n            }}\n          />\n        ) : null}\n\n        <h1\n          style={{\n            margin: 0,\n            display: \"flex\",\n            flexWrap: \"wrap\",\n            justifyContent: \"center\",\n            gap: `${0.08 * fontSize}px ${0.26 * fontSize}px`,\n            color: palette.fg,\n            fontSize,\n            fontWeight: 700,\n            lineHeight: 1.04,\n            letterSpacing: \"-0.03em\",\n          }}\n        >\n          {words.map((word, index) => {\n            const wordIn = ease(\n              T.title + index * T.wordStagger,\n              T.title + index * T.wordStagger + 0.5,\n            );\n            return (\n              // Word-level masks: the fitted line assembles instead of scaling\n              // in, which would fight the whole point of fitting it first.\n              <span\n                key={`${word}-${index}`}\n                style={{ display: \"block\", overflow: \"hidden\" }}\n              >\n                <span\n                  style={{\n                    display: \"block\",\n                    transform: `translateY(${(1 - wordIn) * 100}%)`,\n                  }}\n                >\n                  {word}\n                </span>\n              </span>\n            );\n          })}\n        </h1>\n\n        {subtitle ? (\n          <p\n            style={{\n              margin: 0,\n              maxWidth: stage.w * 0.74,\n              color: palette.dim,\n              fontSize: Math.max(fontSize * 0.3, 22 * u),\n              fontWeight: 500,\n              lineHeight: 1.35,\n              opacity: subtitleIn,\n              transform: `translateY(${(1 - subtitleIn) * 12 * u}px)`,\n            }}\n          >\n            {subtitle}\n          </p>\n        ) : null}\n      </div>\n    </div>\n  );\n};\n"
    }
  ],
  "atlas": {
    "lane": "blocks",
    "drive": "data",
    "tier": "advanced"
  }
}