{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "type-wall",
  "title": "Type Wall",
  "description": "Rows of one word fill the frame, run in alternating directions at different speeds in heavy, narrow and outline faces, with an accent band through the middle.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/type-wall/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadAnton } from \"@remotion/google-fonts/Anton\";\nimport { loadFont as loadInter } from \"@remotion/google-fonts/Inter\";\nimport { useId } from \"react\";\nimport {\n  Easing,\n  interpolate,\n  random,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst { fontFamily: INTER } = loadInter(\"normal\", {\n  weights: [\"900\"],\n  subsets: [\"latin\"],\n});\nconst { fontFamily: ANTON } = loadAnton(\"normal\", {\n  weights: [\"400\"],\n  subsets: [\"latin\"],\n});\n\nexport type TypeWallStyle = \"solid\" | \"narrow\" | \"outline\";\n\nexport interface TypeWallProps {\n  text?: string;\n  separator?: string;\n  fontSize?: number;\n  color?: string;\n  accent?: string;\n  accentText?: string;\n  seed?: number;\n  speed?: number;\n  className?: string;\n}\n\nexport const typeWallDefaults = {\n  text: \"MOTION\",\n  separator: \"·\",\n  fontSize: 64,\n  color: \"#fafafa\",\n  accent: \"#D97757\",\n  accentText: \"#0a0a0a\",\n  seed: 7,\n  speed: 1,\n} satisfies Required<Omit<TypeWallProps, \"className\">>;\n\n/** Row faces away from the center, the accent row always solid. */\nexport const typeWallStyles: readonly TypeWallStyle[] = [\n  \"solid\",\n  \"narrow\",\n  \"outline\",\n];\n\n/** Anton sized so its capitals match Inter's cap height (0.7275 / 0.8594 em). */\nconst NARROW_SCALE = 0.7275 / 0.8594;\n/** Tokens per second at speed 1, before each row's seeded factor. */\nconst TOKENS_PER_SECOND = 0.22;\nconst ENTRANCE = 20;\nconst STAGGER = 2;\n/** Tokens a row streams past on its way in, on top of its cruise. */\nconst ENTRANCE_BURST = 2.5;\nconst BAND = [6, 20] as const;\n/** Lower bound on a token's width per character, in em, for the repeat count. */\nconst MIN_CHAR_WIDTH = 0.4;\nconst MAX_TOKENS = 48;\n\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\nconst easeOutCubic = Easing.out(Easing.cubic);\n\nfunction finite(value: number | undefined, fallback: number) {\n  return typeof value === \"number\" && Number.isFinite(value) ? value : fallback;\n}\n\nfunction fract(x: number) {\n  return x - Math.floor(x);\n}\n\n/** Maps a composition frame onto the 30 fps timeline, scaled by `speed`. */\nexport function getTypeWallTime(\n  frame: number,\n  options: { fps?: number; speed?: number } = {},\n) {\n  const fps = Math.max(1, finite(options.fps, 30));\n  const speed = Math.max(0, finite(options.speed, 1));\n  return (Math.max(0, finite(frame, 0)) * 30 * speed) / fps;\n}\n\n/** Row indices from the top of the frame to the bottom; row 0 is centered. */\nexport function getTypeWallRows(height: number, rowHeight: number) {\n  const reach = Math.ceil((height / 2 - rowHeight / 2) / rowHeight) + 1;\n  const rows: number[] = [];\n  for (let k = -reach; k <= reach; k++) rows.push(k);\n  return rows;\n}\n\nexport function getTypeWallStyle(row: number): TypeWallStyle {\n  if (row === 0) return \"solid\";\n  const n = typeWallStyles.length;\n  return typeWallStyles[((row % n) + n) % n];\n}\n\n/** −1 runs left, 1 runs right. The accent row runs left. */\nexport function getTypeWallDirection(row: number) {\n  return Math.abs(row) % 2 === 0 ? -1 : 1;\n}\n\n/** Cruise speed in tokens per frame and the row's starting phase. */\nexport function getTypeWallMotion(row: number, seed: number) {\n  const factor = 0.7 + 0.6 * random(`type-wall-${seed}-speed-${row}`);\n  return {\n    velocity: (TOKENS_PER_SECOND * factor) / 30,\n    phase: random(`type-wall-${seed}-phase-${row}`),\n  };\n}\n\nexport interface TypeWallRowState {\n  direction: number;\n  /** Translation of the token strip in tokens, always in [−1, 0). */\n  shift: number;\n  /** Share of the row revealed from its entering side, 0..1. */\n  reveal: number;\n}\n\nexport function getTypeWallRow(\n  t: number,\n  row: number,\n  seed = typeWallDefaults.seed,\n): TypeWallRowState {\n  const direction = getTypeWallDirection(row);\n  const { velocity, phase } = getTypeWallMotion(row, seed);\n  const e = easeOutCubic(\n    Math.min(1, Math.max(0, (t - STAGGER * Math.abs(row)) / ENTRANCE)),\n  );\n  const travel = phase + velocity * t + ENTRANCE_BURST * e;\n  // Leftward rows slide the strip left; rightward rows run it from −1 up to 0.\n  const shift = direction < 0 ? -fract(travel) : fract(travel) - 1;\n  return { direction, shift: shift || 0, reveal: e };\n}\n\n/** The accent band growing from the center, 0..1. */\nexport function getTypeWallBand(t: number) {\n  return interpolate(t, [...BAND], [0, 1], { ...CLAMP, easing: easeOutCubic });\n}\n\n/** Frame where the slowest (outermost) row has finished entering. */\nexport function getTypeWallEntranceEnd(height: number, rowHeight: number) {\n  const rows = getTypeWallRows(height, rowHeight);\n  return ENTRANCE + STAGGER * Math.max(...rows.map(Math.abs));\n}\n\n/** How many tokens a strip needs to cover the width twice over. */\nexport function getTypeWallTokenCount(\n  width: number,\n  fontSize: number,\n  token: string,\n) {\n  const tokenWidth =\n    Math.max(1, Array.from(token).length) * MIN_CHAR_WIDTH * fontSize;\n  return Math.min(MAX_TOKENS, Math.ceil(width / tokenWidth) + 2);\n}\n\nexport function TypeWall({\n  text = typeWallDefaults.text,\n  separator = typeWallDefaults.separator,\n  fontSize = typeWallDefaults.fontSize,\n  color = typeWallDefaults.color,\n  accent = typeWallDefaults.accent,\n  accentText = typeWallDefaults.accentText,\n  seed = typeWallDefaults.seed,\n  speed = typeWallDefaults.speed,\n  className,\n}: TypeWallProps) {\n  const frame = useCurrentFrame();\n  const { fps, width, height } = useVideoConfig();\n  const unit = height / 720;\n  const rowHeight = Math.max(8, fontSize * unit);\n  const t = getTypeWallTime(frame, { fps, speed });\n  const rows = getTypeWallRows(height, rowHeight);\n  const token = `${text} ${separator} `;\n  const band = getTypeWallBand(t);\n  const outlineId = `type-wall-${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n\n  return (\n    <div\n      className={className}\n      style={{ position: \"absolute\", inset: 0, overflow: \"hidden\" }}\n    >\n      {/* Outline from the filled silhouette: the variable font's overlapping\n          contours would show through a text stroke. */}\n      <svg width={0} height={0} style={{ position: \"absolute\" }} aria-hidden>\n        <defs>\n          <filter id={outlineId}>\n            <feMorphology\n              in=\"SourceAlpha\"\n              operator=\"erode\"\n              radius={1.5 * unit}\n              result=\"inner\"\n            />\n            <feComposite\n              in=\"SourceAlpha\"\n              in2=\"inner\"\n              operator=\"out\"\n              result=\"ring\"\n            />\n            <feFlood floodColor={color} />\n            <feComposite in2=\"ring\" operator=\"in\" />\n          </filter>\n        </defs>\n      </svg>\n      {rows.map((row) => {\n        const style = getTypeWallStyle(row);\n        const state = getTypeWallRow(t, row, seed);\n        const narrow = style === \"narrow\";\n        const size = narrow ? rowHeight * NARROW_SCALE : rowHeight;\n        const count = getTypeWallTokenCount(width, size, token);\n        const onBand = row === 0;\n        const hidden = `${(1 - state.reveal) * 100}%`;\n        return (\n          <div\n            key={row}\n            style={{\n              position: \"absolute\",\n              left: 0,\n              right: 0,\n              top: height / 2 + (row - 0.5) * rowHeight,\n              height: rowHeight,\n              clipPath:\n                state.direction < 0\n                  ? `inset(0 0 0 ${hidden})`\n                  : `inset(0 ${hidden} 0 0)`,\n            }}\n          >\n            {onBand ? (\n              <div\n                style={{\n                  position: \"absolute\",\n                  inset: 0,\n                  background: accent,\n                  transform: `scaleX(${band})`,\n                }}\n              />\n            ) : null}\n            <div\n              style={{\n                position: \"absolute\",\n                left: 0,\n                top: 0,\n                width: \"max-content\",\n                height: \"100%\",\n                display: \"flex\",\n                alignItems: \"center\",\n                whiteSpace: \"pre\",\n                fontFamily: narrow ? ANTON : INTER,\n                fontSize: size,\n                lineHeight: 1,\n                fontWeight: narrow ? 400 : 900,\n                color: onBand ? accentText : color,\n                filter: style === \"outline\" ? `url(#${outlineId})` : undefined,\n                transform: `translateX(${(state.shift * 100) / count}%)`,\n              }}\n            >\n              {Array.from({ length: count }, (_, i) => (\n                <span key={i}>{token}</span>\n              ))}\n            </div>\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/type-wall.tsx"
    }
  ],
  "type": "registry:component"
}
