{
  "name": "callout-spotlight",
  "type": "registry:block",
  "description": "Spotlight that is aimed: the mask closes from the whole frame onto the target, a ring pings it, then a connector draws out to the callout",
  "dependencies": [
    "remotion",
    "@remotion/google-fonts"
  ],
  "registryDependencies": [
    "code-syntax",
    "layout",
    "motion-tokens"
  ],
  "files": [
    {
      "path": "registry/bases/default/scenes/callout-spotlight/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 { clampRectToSafeArea, 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 SpotlightTarget = {\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n};\n\nexport type CalloutSpotlightProps = {\n  title: string;\n  subtitle?: string;\n  /** Small label above the title. */\n  kicker?: string;\n  /** Region to spotlight, in source pixels. */\n  target: SpotlightTarget;\n  /** Screenshot or capture under the spotlight. */\n  backgroundSrc?: string;\n  /** Size the target was measured against. Defaults to the composition. */\n  sourceWidth?: number;\n  sourceHeight?: number;\n  /** How far the rest of the frame is knocked back, 0–1. */\n  dim?: 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  background: 0,\n  /** The mask closes in from the whole frame onto the target. */\n  iris: 0.3,\n  irisFor: 0.7,\n  ping: 1.0,\n  pingFor: 0.9,\n  connector: 1.05,\n  card: 1.2,\n  kicker: 1.3,\n  title: 1.4,\n  subtitle: 1.6,\n} as const;\n\nconst clamp = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\n/**\n * A spotlight that is aimed rather than drawn: the frame dims and the mask\n * closes in from the whole picture onto the target, a ring pings the region\n * once it lands, and only then does a connector draw out to the callout — so\n * the eye is taken to the thing before it is told about it.\n */\nexport const CalloutSpotlight: React.FC<CalloutSpotlightProps> = ({\n  title,\n  subtitle,\n  kicker,\n  target,\n  backgroundSrc,\n  sourceWidth,\n  sourceHeight,\n  dim = 0.72,\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 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\n  // The target is authored against whatever the capture was measured at, so it\n  // is mapped into the composition before anything else uses it — through the\n  // same `object-fit: cover` transform the background image gets, otherwise the\n  // spotlight lands next to the thing it is meant to be aiming at whenever the\n  // capture and the composition disagree on aspect.\n  const sw = sourceWidth ?? width;\n  const sh = sourceHeight ?? height;\n  const scale = Math.max(width / sw, height / sh);\n  const offsetX = (width - sw * scale) / 2;\n  const offsetY = (height - sh * scale) / 2;\n  const rect = clampRectToSafeArea(\n    {\n      x: offsetX + target.x * scale,\n      y: offsetY + target.y * scale,\n      width: target.width * scale,\n      height: target.height * scale,\n    },\n    width,\n    height,\n  );\n\n  const backgroundIn = ease(T.background, T.background + 0.6, EASING.editorial);\n  const iris = ease(T.iris, T.iris + T.irisFor, EASING.editorial);\n  const ping = ease(T.ping, T.ping + T.pingFor, EASING.editorial);\n  const connector = ease(T.connector, T.connector + 0.45, EASING.editorial);\n  const card = spring({\n    frame: frame - at(T.card),\n    fps,\n    config: { damping: 16, stiffness: 150, mass: 0.7 },\n  });\n  const kickerIn = kicker ? ease(T.kicker, T.kicker + 0.4) : 0;\n  const titleIn = ease(T.title, T.title + 0.45);\n  const subtitleIn = subtitle ? ease(T.subtitle, T.subtitle + 0.45) : 0;\n\n  // The hole starts as the whole frame and closes onto the target.\n  const hole = {\n    x: interpolate(iris, [0, 1], [0, rect.x]),\n    y: interpolate(iris, [0, 1], [0, rect.y]),\n    width: interpolate(iris, [0, 1], [width, rect.width]),\n    height: interpolate(iris, [0, 1], [height, rect.height]),\n  };\n\n  // The callout sits under the target when there is room, otherwise above it.\n  // When it sits above, it is anchored by its foot rather than its head, so no\n  // guess at the card's height is needed anywhere.\n  const cardW = Math.min(width - safe.paddingLeft - safe.paddingRight, 470 * u);\n  const gap = 46 * u;\n  const cardBelow =\n    height - (rect.y + rect.height) > safe.paddingBottom + 210 * u;\n  const cardX = Math.min(\n    Math.max(rect.x + rect.width / 2 - cardW / 2, safe.paddingLeft),\n    width - safe.paddingRight - cardW,\n  );\n  /** The connector spans the gap next to the target, whatever the card's size. */\n  const connectorTop = cardBelow ? rect.y + rect.height : rect.y - gap;\n  const connectorX = Math.min(\n    Math.max(rect.x + rect.width / 2, cardX + 34 * u),\n    cardX + cardW - 34 * u,\n  );\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      }}\n    >\n      {backgroundSrc ? (\n        <Img\n          src={backgroundSrc}\n          style={{\n            width: \"100%\",\n            height: \"100%\",\n            objectFit: \"cover\",\n            transform: `scale(${interpolate(backgroundIn, [0, 1], [1.04, 1])})`,\n          }}\n        />\n      ) : null}\n\n      {/* Everything outside the hole is knocked back by one huge shadow */}\n      <div\n        style={{\n          position: \"absolute\",\n          left: hole.x,\n          top: hole.y,\n          width: hole.width,\n          height: hole.height,\n          borderRadius: 14 * u * iris,\n          boxShadow: `0 0 0 ${Math.max(width, height)}px rgba(4,5,9,${\n            dim * iris\n          })`,\n          border: `${2 * u}px solid ${accentColor}${\n            iris > 0.6 ? \"CC\" : \"00\"\n          }`,\n        }}\n      />\n\n      {/* Ping — one ring off the target once the mask has landed */}\n      <div\n        style={{\n          position: \"absolute\",\n          left: rect.x,\n          top: rect.y,\n          width: rect.width,\n          height: rect.height,\n          borderRadius: 14 * u,\n          border: `${2 * u}px solid ${accentColor}`,\n          opacity: ping > 0 && ping < 1 ? (1 - ping) * 0.85 : 0,\n          transform: `scale(${interpolate(ping, [0, 1], [1, 1.12])})`,\n        }}\n      />\n\n      {/* Connector from the callout to the region it is about */}\n      <div\n        style={{\n          position: \"absolute\",\n          left: connectorX - 1 * u,\n          top: connectorTop,\n          width: 2 * u,\n          height: gap,\n          background: accentColor,\n          transformOrigin: cardBelow ? \"top center\" : \"bottom center\",\n          transform: `scaleY(${connector})`,\n          opacity: 0.85,\n        }}\n      />\n      <div\n        style={{\n          position: \"absolute\",\n          left: connectorX - 5 * u,\n          top: (cardBelow ? rect.y + rect.height : rect.y) - 5 * u,\n          width: 10 * u,\n          height: 10 * u,\n          borderRadius: \"50%\",\n          background: accentColor,\n          opacity: connector,\n        }}\n      />\n\n      <div\n        style={{\n          position: \"absolute\",\n          left: cardX,\n          ...(cardBelow\n            ? { top: rect.y + rect.height + gap }\n            : { bottom: height - (rect.y - gap) }),\n          width: cardW,\n          borderRadius: 18 * u,\n          background: `${palette.window}F2`,\n          border: `1px solid ${palette.border}`,\n          boxShadow: `inset 0 1px 0 ${palette.highlight}, 0 ${\n            22 * u\n          }px ${56 * u}px ${palette.shadow}`,\n          padding: `${20 * u}px ${24 * u}px`,\n          opacity: Math.min(card, 1),\n          transform: `translateY(${\n            (1 - Math.min(card, 1)) * (cardBelow ? 18 : -18) * u\n          }px)`,\n        }}\n      >\n        {kicker ? (\n          <div\n            style={{\n              color: accentColor,\n              fontSize: 18 * u,\n              fontWeight: 600,\n              letterSpacing: \"0.12em\",\n              textTransform: \"uppercase\",\n              opacity: kickerIn,\n            }}\n          >\n            {kicker}\n          </div>\n        ) : null}\n        <div style={{ overflow: \"hidden\", marginTop: kicker ? 8 * u : 0 }}>\n          <div\n            style={{\n              color: palette.fg,\n              fontSize: 34 * u,\n              fontWeight: 700,\n              lineHeight: 1.14,\n              letterSpacing: \"-0.02em\",\n              transform: `translateY(${(1 - titleIn) * 100}%)`,\n            }}\n          >\n            {title}\n          </div>\n        </div>\n        {subtitle ? (\n          <div\n            style={{\n              marginTop: 8 * u,\n              color: palette.dim,\n              fontSize: 22 * u,\n              lineHeight: 1.35,\n              opacity: subtitleIn,\n            }}\n          >\n            {subtitle}\n          </div>\n        ) : null}\n      </div>\n    </div>\n  );\n};\n"
    }
  ],
  "atlas": {
    "lane": "blocks",
    "drive": "time",
    "tier": "core"
  }
}