{
  "name": "lower-third",
  "type": "registry:block",
  "description": "Name plate that plays its whole life: wipes out from the frame edge, settles, holds, then retreats the way it came",
  "dependencies": [
    "remotion",
    "@remotion/google-fonts"
  ],
  "registryDependencies": [
    "code-syntax",
    "layout",
    "motion-tokens"
  ],
  "files": [
    {
      "path": "registry/bases/default/scenes/lower-third/index.tsx",
      "type": "registry:block",
      "content": "import { loadFont } from \"@remotion/google-fonts/Inter\";\nimport {\n  AbsoluteFill,\n  interpolate,\n  spring,\n  useCurrentFrame,\n  useVideoConfig,\n} 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 LowerThirdProps = {\n  title: string;\n  subtitle?: string;\n  /** Small tag on the plate — LIVE, EP 12, the segment name. */\n  badge?: string;\n  /** Which edge the plate slides out from. */\n  align?: \"left\" | \"right\";\n  /**\n   * Seconds the plate holds before it retreats. Omit to leave it on screen for\n   * the rest of the scene.\n   */\n  holdSeconds?: 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  /** Plate wipes out from the frame edge. */\n  plate: 0,\n  plateFor: 0.5,\n  title: 0.22,\n  subtitle: 0.38,\n  badge: 0.5,\n  /** Retreat, once the hold is over. */\n  exitFor: 0.42,\n} as const;\n\nconst clamp = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\n/**\n * A name plate that plays its whole life: it wipes out from the frame edge, the\n * name and role settle inside it, it holds, and then it retreats the way it\n * came — rather than fading on and being left standing for the rest of the cut.\n */\nexport const LowerThird: React.FC<LowerThirdProps> = ({\n  title,\n  subtitle,\n  badge,\n  align = \"left\",\n  holdSeconds,\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 portrait = height > width;\n  const u = portrait\n    ? Math.min(width / 620, height / 1120)\n    : Math.min(width / 1280, height / 720);\n  const isRight = align === \"right\";\n\n  const plate = spring({\n    frame: frame - at(T.plate),\n    fps,\n    config: { damping: 17, stiffness: 130, mass: 0.8 },\n  });\n  const open = ease(T.plate, T.plate + T.plateFor, EASING.editorial);\n  const titleIn = ease(T.title, T.title + 0.42);\n  const subtitleIn = subtitle ? ease(T.subtitle, T.subtitle + 0.42) : 0;\n  const badgeIn = badge ? ease(T.badge, T.badge + 0.4) : 0;\n  // Exits accelerate away; entrances decelerate in. Never ease-out an exit.\n  const exit =\n    holdSeconds === undefined\n      ? 0\n      : interpolate(\n          frame,\n          [at(holdSeconds), at(holdSeconds + T.exitFor)],\n          [0, 1],\n          { easing: EASING.exit, ...clamp },\n        );\n\n  const plateW = Math.min(width - safe.paddingLeft - safe.paddingRight, 660 * u);\n\n  return (\n    <AbsoluteFill style={{ pointerEvents: \"none\", fontFamily }}>\n      <div\n        style={{\n          position: \"absolute\",\n          left: safe.paddingLeft,\n          right: safe.paddingRight,\n          bottom: Math.max(safe.paddingBottom, 74 * u),\n          display: \"flex\",\n          justifyContent: isRight ? \"flex-end\" : \"flex-start\",\n        }}\n      >\n        <div\n          style={{\n            // Hugs its content: a name plate stretched to a fixed width leaves\n            // a long empty bar next to a short name.\n            width: \"fit-content\",\n            maxWidth: plateW,\n            borderRadius: 16 * u,\n            background: backgroundColor ?? `${palette.window}F0`,\n            border: `1px solid ${palette.border}`,\n            boxShadow: `inset 0 1px 0 ${palette.highlight}, 0 ${\n              20 * u\n            }px ${52 * u}px ${palette.shadow}`,\n            padding: `${17 * u}px ${24 * u}px`,\n            // The plate wipes open from its own edge, then retreats the same way.\n            clipPath: `inset(0 ${isRight ? 0 : (1 - open) * 100}% 0 ${\n              isRight ? (1 - open) * 100 : 0\n            }% round ${16 * u}px)`,\n            opacity: 1 - exit,\n            transform: `translateX(${\n              (1 - plate) * (isRight ? 40 : -40) * u +\n              exit * (isRight ? 60 : -60) * u\n            }px)`,\n            display: \"flex\",\n            alignItems: \"center\",\n            gap: 16 * u,\n            overflow: \"hidden\",\n          }}\n        >\n          <div style={{ minWidth: 0, flex: 1 }}>\n            {/* Name rises out of its own mask */}\n            <div style={{ overflow: \"hidden\", paddingBottom: \"0.04em\" }}>\n              <div\n                style={{\n                  color: palette.fg,\n                  fontSize: 34 * u,\n                  fontWeight: 700,\n                  lineHeight: 1.1,\n                  letterSpacing: \"-0.02em\",\n                  whiteSpace: \"nowrap\",\n                  textOverflow: \"ellipsis\",\n                  overflow: \"hidden\",\n                  transform: `translateY(${(1 - titleIn) * 100}%)`,\n                }}\n              >\n                {title}\n              </div>\n            </div>\n            {subtitle ? (\n              <div\n                style={{\n                  marginTop: 4 * u,\n                  color: palette.dim,\n                  fontSize: 22 * u,\n                  fontWeight: 500,\n                  whiteSpace: \"nowrap\",\n                  textOverflow: \"ellipsis\",\n                  overflow: \"hidden\",\n                  opacity: subtitleIn,\n                  transform: `translateY(${(1 - subtitleIn) * 8 * u}px)`,\n                }}\n              >\n                {subtitle}\n              </div>\n            ) : null}\n          </div>\n\n          {badge ? (\n            <div\n              style={{\n                flexShrink: 0,\n                padding: `${7 * u}px ${13 * u}px`,\n                borderRadius: 999,\n                background: `${accentColor}1E`,\n                border: `1px solid ${accentColor}66`,\n                color: accentColor,\n                fontSize: 17 * u,\n                fontWeight: 600,\n                letterSpacing: \"0.1em\",\n                textTransform: \"uppercase\",\n                opacity: badgeIn,\n                transform: `scale(${interpolate(badgeIn, [0, 1], [0.86, 1])})`,\n              }}\n            >\n              {badge}\n            </div>\n          ) : null}\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n};\n"
    }
  ],
  "atlas": {
    "lane": "blocks",
    "drive": "time",
    "tier": "core"
  }
}