{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "point-intro",
  "title": "Point Intro",
  "description": "A headline is written word by word and a colored point lands after it: 'Introducing remocn 2.0.'.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/point-intro/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont } from \"@remotion/google-fonts/Figtree\";\nimport { useLayoutEffect, useRef, useState } from \"react\";\nimport {\n  AbsoluteFill,\n  continueRender,\n  delayRender,\n  Easing,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst face = loadFont(\"normal\", {\n  weights: [\"400\"],\n  subsets: [\"latin\"],\n});\n\nexport interface PointIntroProps {\n  /** The headline, written out word by word. The point lands after it. */\n  title?: string;\n  /** Small grey line under the headline. */\n  subline?: string;\n  /** Colour of the point. */\n  accent?: string;\n  theme?: \"dark\" | \"light\";\n  speed?: number;\n}\n\nconst THEMES = {\n  light: { bg: \"#FAFAF9\", fg: \"#0E0E0D\", muted: \"#8C8C88\" },\n  dark: { bg: \"#0A0A0A\", fg: \"#F2F2F0\", muted: \"#7C7C79\" },\n} as const;\n\nconst EASE_OUT = Easing.bezier(0.22, 1, 0.36, 1);\nconst EASE_IN_OUT = Easing.bezier(0.65, 0, 0.35, 1);\n\n// Beats, in frames at 30fps (before `speed`).\nconst DOT_IN = 0;\nconst BREATHE = 14;\nconst BREATHE_LEN = 30;\nconst GLIDE = 46;\nconst GLIDE_LEN = 26;\nconst WRITE = 66;\nconst WORD_STAGGER = 13;\nconst WORD_LEN = 26;\nconst DOT_STEP = 18;\n\n/** Diameter of the point, in em of the headline. Matches a Figtree full stop. */\nconst DOT_EM = 0.118;\n/** How much larger the point is while it is alone on the stage. */\nconst SOLO_SCALE = 2.1;\n/** Room around the masked words so blur and rise are never clipped, in em. */\nconst MASK_PAD = 0.4;\n\ninterface Layout {\n  rowW: number;\n  rowH: number;\n  baseline: number;\n  wordLeft: number[];\n  wordRight: number[];\n  /** Centre x of the reserved full stop. */\n  stopX: number;\n}\n\nfunction sameLayout(a: Layout | null, b: Layout) {\n  return (\n    a !== null &&\n    a.rowW === b.rowW &&\n    a.baseline === b.baseline &&\n    a.stopX === b.stopX &&\n    a.wordRight.join() === b.wordRight.join()\n  );\n}\n\nexport function PointIntro({\n  title = \"Introducing remocn 2.0\",\n  subline = \"Animated components for Remotion. Free and open source.\",\n  accent = \"#3B5BFD\",\n  theme = \"light\",\n  speed = 1,\n}: PointIntroProps) {\n  const frame = useCurrentFrame() * speed;\n  const { width, height } = useVideoConfig();\n  const t = THEMES[theme];\n  const words = title.split(/\\s+/).filter(Boolean);\n\n  const fontSize = Math.round(width * 0.052);\n  const subSize = Math.round(width * 0.0165);\n  const dot = fontSize * DOT_EM;\n\n  const rowRef = useRef<HTMLDivElement>(null);\n  const wordRefs = useRef<(HTMLSpanElement | null)[]>([]);\n  const stopRef = useRef<HTMLSpanElement>(null);\n  const baseRef = useRef<HTMLSpanElement>(null);\n  const [layout, setLayout] = useState<Layout | null>(null);\n  const [handle] = useState(() => delayRender(\"point-intro: measuring\"));\n  const released = useRef(false);\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: re-measure when the copy or size changes\n  useLayoutEffect(() => {\n    let live = true;\n    face\n      .waitUntilDone()\n      .catch(() => undefined)\n      .then(() => document.fonts.ready)\n      .then(() => {\n        if (!live) return;\n        const row = rowRef.current;\n        const stop = stopRef.current;\n        const base = baseRef.current;\n        if (row && stop && base) {\n          const els = wordRefs.current.slice(0, words.length);\n          const next: Layout = {\n            rowW: row.offsetWidth,\n            rowH: row.offsetHeight,\n            baseline: base.offsetTop,\n            wordLeft: els.map((el) => el?.offsetLeft ?? 0),\n            wordRight: els.map((el) =>\n              el ? el.offsetLeft + el.offsetWidth : 0,\n            ),\n            stopX: stop.offsetLeft + stop.offsetWidth / 2,\n          };\n          setLayout((prev) => (sameLayout(prev, next) ? prev : next));\n        }\n        if (!released.current) {\n          released.current = true;\n          continueRender(handle);\n        }\n      });\n    return () => {\n      live = false;\n    };\n  }, [title, fontSize, handle]);\n\n  // The point: in, one breath, a glide to the start of the line, then it\n  // leads the writing word by word and settles as the full stop.\n  const dotIn = interpolate(frame, [DOT_IN, DOT_IN + 14], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASE_OUT,\n  });\n  const breath = interpolate(frame, [BREATHE, BREATHE + BREATHE_LEN], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n  const breathe = Math.sin(EASE_IN_OUT(breath) * Math.PI) * 0.38;\n  const glide = interpolate(frame, [GLIDE, GLIDE + GLIDE_LEN], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASE_IN_OUT,\n  });\n  const lastLand = WRITE + (words.length - 1) * WORD_STAGGER + DOT_STEP;\n  const settle = interpolate(frame, [GLIDE, lastLand], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASE_IN_OUT,\n  });\n  const dotScale =\n    interpolate(dotIn, [0, 1], [0.6, 1]) *\n    interpolate(settle, [0, 1], [SOLO_SCALE, 1]) *\n    (1 + breathe);\n\n  let dotX = 0;\n  let dotY = 0;\n  if (layout) {\n    const stopOffset = layout.stopX - (layout.wordRight.at(-1) ?? 0);\n    const startX = (layout.wordLeft[0] ?? 0) - stopOffset;\n    const centreX = layout.rowW / 2;\n    const centreY = layout.rowH / 2;\n    const baseY = layout.baseline - dot / 2;\n    let x = interpolate(glide, [0, 1], [centreX, startX]);\n    let prev = startX;\n    words.forEach((_, i) => {\n      const target =\n        i === words.length - 1\n          ? layout.stopX\n          : (layout.wordRight[i] ?? 0) + stopOffset;\n      const p = interpolate(\n        frame,\n        [WRITE + i * WORD_STAGGER, WRITE + i * WORD_STAGGER + DOT_STEP],\n        [0, 1],\n        {\n          extrapolateLeft: \"clamp\",\n          extrapolateRight: \"clamp\",\n          easing: EASE_IN_OUT,\n        },\n      );\n      x += (target - prev) * p;\n      prev = target;\n    });\n    dotX = x;\n    dotY = interpolate(glide, [0, 1], [centreY, baseY]);\n  }\n\n  // The point writes: text exists only behind it, surfacing through a soft\n  // edge that ends just past its left side. Once it lands, the edge opens.\n  const pad = MASK_PAD * fontSize;\n  const opened = interpolate(frame, [lastLand - 4, lastLand + 8], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n  const frontier = layout\n    ? dotX - dot / 2 + pad + fontSize * 0.06 + opened * fontSize\n    : -1e4;\n  const mask = `linear-gradient(90deg, #000 ${\n    frontier - fontSize * 0.16\n  }px, transparent ${frontier}px)`;\n\n  const subStart = lastLand + 10;\n  const subP = interpolate(frame, [subStart, subStart + 30], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASE_OUT,\n  });\n\n  return (\n    <AbsoluteFill\n      style={{\n        background: t.bg,\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        fontFamily: face.fontFamily,\n      }}\n    >\n      <div\n        ref={rowRef}\n        style={{\n          position: \"relative\",\n          whiteSpace: \"nowrap\",\n          fontSize,\n          fontWeight: 400,\n          letterSpacing: \"-0.028em\",\n          lineHeight: 1.2,\n          color: t.fg,\n          // Optical centring: the line sits a touch above the true middle so\n          // the subline below balances it.\n          marginTop: -height * 0.03,\n        }}\n      >\n        <span\n          style={{\n            display: \"inline-block\",\n            padding: `${MASK_PAD}em`,\n            margin: `-${MASK_PAD}em`,\n            maskImage: mask,\n            WebkitMaskImage: mask,\n          }}\n        >\n          {words.map((w, i) => {\n            const start = WRITE + i * WORD_STAGGER;\n            const p = interpolate(frame, [start, start + WORD_LEN], [0, 1], {\n              extrapolateLeft: \"clamp\",\n              extrapolateRight: \"clamp\",\n              easing: EASE_OUT,\n            });\n            return (\n              <span key={`${w}-${i}`}>\n                <span\n                  ref={(el) => {\n                    wordRefs.current[i] = el;\n                  }}\n                  style={{\n                    display: \"inline-block\",\n                    opacity: p,\n                    filter: `blur(${(1 - p) * 10}px)`,\n                    transform: `translateY(${(1 - p) * 0.24}em)`,\n                  }}\n                >\n                  {w}\n                </span>\n                {i < words.length - 1 ? \" \" : null}\n              </span>\n            );\n          })}\n        </span>\n        <span ref={stopRef} style={{ display: \"inline-block\", opacity: 0 }}>\n          .\n        </span>\n        <span\n          ref={baseRef}\n          style={{ display: \"inline-block\", width: 0, height: 0 }}\n        />\n        <div\n          style={{\n            position: \"absolute\",\n            left: dotX - dot / 2,\n            top: dotY - dot / 2,\n            width: dot,\n            height: dot,\n            borderRadius: \"50%\",\n            background: accent,\n            opacity: layout ? dotIn : 0,\n            transform: `scale(${dotScale})`,\n          }}\n        />\n        <div\n          style={{\n            position: \"absolute\",\n            left: \"50%\",\n            top: \"100%\",\n            marginTop: fontSize * 0.42,\n            transform: `translate(-50%, ${(1 - subP) * 12}px)`,\n            fontSize: subSize,\n            letterSpacing: \"-0.005em\",\n            lineHeight: 1.4,\n            color: t.muted,\n            opacity: subP,\n            filter: `blur(${(1 - subP) * 8}px)`,\n          }}\n        >\n          {subline}\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/point-intro.tsx"
    }
  ],
  "type": "registry:component"
}
