{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tile-wave",
  "title": "Tile Wave",
  "description": "A wave of tiles flips across the frame, swapping headline for scene on a stage.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/tile-wave/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadDisplay } from \"@remotion/google-fonts/SpaceGrotesk\";\nimport type React from \"react\";\nimport { useId } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst { fontFamily: display } = loadDisplay(\"normal\", {\n  weights: [\"500\"],\n  subsets: [\"latin\"],\n});\n\nexport interface TileWaveProps {\n  /** Outgoing scene: a short headline string or any React node. */\n  from?: React.ReactNode;\n  /** Incoming scene, set on a \"beam\" gradient panel. */\n  to?: React.ReactNode;\n  /** Tile rows; columns follow the aspect ratio. */\n  rows?: number;\n  /** Frames the outgoing scene holds before the wave. */\n  delay?: number;\n  accent?: string;\n  /** Playback multiplier. */\n  speed?: number;\n  /** Stage seen between the tiles; the outgoing scene takes the other tone. */\n  theme?: \"dark\" | \"light\";\n}\n\nconst STAGE = { dark: \"#0A0A0B\", light: \"#F5F4F0\" } as const;\nconst INK = { dark: \"#F3F2EF\", light: \"#121214\" } as const;\n\nconst SPREAD = 26;\nconst TILE = 22;\nconst SHRINK_END = 0.46;\nconst GROW_START = 0.54;\n\nconst EASE_SHRINK = Easing.bezier(0.4, 0, 0.7, 0.5);\nconst EASE_GROW = Easing.bezier(0.22, 1, 0.36, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\nexport const tileWaveLength = (delay = 16) => delay + SPREAD + TILE + 30;\n\n/* gradients.sh \"beam\" palette: deep indigo into the remocn blue, a cold\n   highlight rising from the lower right. Drifts a few percent per shot. */\nconst BEAM = [\"#f4f7ff\", \"#8fb4ff\", \"#3d5bff\", \"#1a1660\"] as const;\n\nconst mixA = (c: string, a: number) =>\n  `color-mix(in oklab, ${c} ${Math.round(a * 100)}%, transparent)`;\n\nfunction beamGround(d: number) {\n  const [c0, c1, c2, c3] = BEAM;\n  const s = Math.sin(d * Math.PI) * 4;\n  return [\n    `radial-gradient(70% 90% at ${(86 - s).toFixed(2)}% ${(108 - s).toFixed(2)}%, ${mixA(c0, 0.55)} 0%, ${mixA(c1, 0.45)} 30%, transparent 68%)`,\n    `radial-gradient(60% 70% at ${(8 + s).toFixed(2)}% 0%, ${mixA(c3, 0.9)} 0%, transparent 72%)`,\n    `linear-gradient(${(158 + s).toFixed(2)}deg, ${c3} 0%, ${c2} 52%, ${c2} 70%, ${c1} 100%)`,\n  ].join(\", \");\n}\n\nfunction Headline({\n  text,\n  bg,\n  fg,\n  dotColor,\n}: {\n  text: string;\n  bg: string;\n  fg: string;\n  dotColor: string;\n}) {\n  const { width: W, height: H } = useVideoConfig();\n  const u = H / 720;\n  const dot = text.endsWith(\".\");\n  const body = dot ? text.slice(0, -1) : text;\n  const size = Math.min(\n    196 * u,\n    (0.74 * W) / (Math.max(4, text.length) * 0.56),\n  );\n  return (\n    <AbsoluteFill\n      style={{\n        background: bg,\n        color: fg,\n        alignItems: \"center\",\n        justifyContent: \"center\",\n      }}\n    >\n      <div\n        style={{\n          fontFamily: display,\n          fontWeight: 500,\n          fontSize: size,\n          letterSpacing: \"-0.04em\",\n          lineHeight: 1,\n          whiteSpace: \"nowrap\",\n          paddingBottom: size * 0.08,\n          paddingRight: size * 0.04,\n        }}\n      >\n        {body}\n        {dot ? <span style={{ color: dotColor }}>.</span> : null}\n      </div>\n    </AbsoluteFill>\n  );\n}\n\nfunction Scene({\n  content,\n  bg,\n  fg,\n  dotColor,\n}: {\n  content: React.ReactNode;\n  bg: string;\n  fg: string;\n  dotColor: string;\n}) {\n  if (typeof content === \"string\" || typeof content === \"number\") {\n    return (\n      <Headline text={String(content)} bg={bg} fg={fg} dotColor={dotColor} />\n    );\n  }\n  return <AbsoluteFill style={{ background: bg }}>{content}</AbsoluteFill>;\n}\n\nfunction Grain({ opacity }: { opacity: number }) {\n  const id = `g${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n  return (\n    <svg\n      width=\"100%\"\n      height=\"100%\"\n      style={{ position: \"absolute\", inset: 0, opacity, pointerEvents: \"none\" }}\n      aria-hidden\n    >\n      <filter id={id}>\n        <feTurbulence\n          type=\"fractalNoise\"\n          baseFrequency=\"0.9\"\n          numOctaves={2}\n          seed={17}\n          stitchTiles=\"stitch\"\n        />\n        <feColorMatrix values=\"0 0 0 0 0.5  0 0 0 0 0.5  0 0 0 0 0.5  0 0 0 0.9 0\" />\n      </filter>\n      <rect width=\"100%\" height=\"100%\" filter={`url(#${id})`} />\n    </svg>\n  );\n}\n\nexport function TileWave({\n  from = \"Idea.\",\n  to = \"Shipped.\",\n  rows = 8,\n  delay = 16,\n  accent = \"#3B5BFD\",\n  speed = 1,\n  theme = \"dark\",\n}: TileWaveProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H } = useVideoConfig();\n  const u = H / 720;\n  const stage = STAGE[theme];\n  const aTone = theme === \"dark\" ? \"light\" : \"dark\";\n  const aBg = STAGE[aTone];\n  const aFg = INK[aTone];\n  const bFg = \"#FFFFFF\";\n\n  const nRows = Math.max(3, Math.round(rows));\n  const nCols = Math.max(3, Math.round((W / H) * nRows));\n  const cw = W / nCols;\n  const ch = H / nRows;\n  const minScale = (7 * u) / Math.min(cw, ch);\n  const bleed = 0.6;\n\n  const sceneA = <Scene content={from} bg={aBg} fg={aFg} dotColor={accent} />;\n  const bGround = beamGround(Math.min(1, t / tileWaveLength(delay)));\n  const sceneB = <Scene content={to} bg={bGround} fg={bFg} dotColor={bFg} />;\n  const before = t <= delay;\n  const after = t >= delay + SPREAD + TILE;\n\n  const tiles: React.ReactNode[] = [];\n  const tileB = BEAM[2];\n  const restA: string[] = [];\n  const restB: string[] = [];\n  const cell = (x: number, y: number) => {\n    const e = bleed;\n    return `M${(x - e).toFixed(2)} ${(y - e).toFixed(2)}h${(cw + 2 * e).toFixed(2)}v${(ch + 2 * e).toFixed(2)}h${(-cw - 2 * e).toFixed(2)}Z`;\n  };\n  for (let r = 0; r < (before || after ? 0 : nRows); r++) {\n    for (let c = 0; c < nCols; c++) {\n      const x = c * cw;\n      const y = r * ch;\n      const diag = (c / (nCols - 1) + r / (nRows - 1)) / 2;\n      const start = delay + diag * SPREAD;\n      const p = interpolate(t, [start, start + TILE], [0, 1], CLAMP);\n      const isB = p >= (SHRINK_END + GROW_START) / 2;\n      let s: number;\n      if (p <= SHRINK_END) {\n        s = 1 - (1 - minScale) * EASE_SHRINK(p / SHRINK_END);\n      } else if (p < GROW_START) {\n        s = minScale;\n      } else {\n        s =\n          minScale +\n          (1 - minScale) * EASE_GROW((p - GROW_START) / (1 - GROW_START));\n      }\n      s = Math.min(1, s * 1.03);\n      if (s >= 1) {\n        (isB ? restB : restA).push(cell(x, y));\n        continue;\n      }\n      const tw = cw * s;\n      const th = ch * s;\n      const round = Math.min(1, (1 - s) / (1 - minScale)) ** 1.6;\n      const radius = Math.min(tw, th) * 0.5 * round;\n      if (s <= 0.14) {\n        tiles.push(\n          <div\n            key={`${r}-${c}`}\n            style={{\n              position: \"absolute\",\n              left: x + (cw - tw) / 2,\n              top: y + (ch - th) / 2,\n              width: tw,\n              height: th,\n              borderRadius: radius,\n              background: isB ? tileB : aBg,\n            }}\n          />,\n        );\n        continue;\n      }\n      const ins = [y, W - x - cw, H - y - ch, x]\n        .map((v) => `${v.toFixed(2)}px`)\n        .join(\" \");\n      tiles.push(\n        <div\n          key={`${r}-${c}`}\n          style={{\n            position: \"absolute\",\n            left: 0,\n            top: 0,\n            width: W,\n            height: H,\n            transform: `scale(${s})`,\n            transformOrigin: `${x + cw / 2}px ${y + ch / 2}px`,\n            clipPath: `inset(${ins} round ${(radius / s).toFixed(2)}px)`,\n          }}\n        >\n          {isB ? sceneB : sceneA}\n        </div>,\n      );\n    }\n  }\n\n  return (\n    <AbsoluteFill style={{ background: stage, overflow: \"hidden\" }}>\n      {before ? sceneA : null}\n      {after ? sceneB : null}\n      {restA.length ? (\n        <AbsoluteFill style={{ clipPath: `path(\"${restA.join(\"\")}\")` }}>\n          {sceneA}\n        </AbsoluteFill>\n      ) : null}\n      {restB.length ? (\n        <AbsoluteFill style={{ clipPath: `path(\"${restB.join(\"\")}\")` }}>\n          {sceneB}\n        </AbsoluteFill>\n      ) : null}\n      {tiles}\n      <Grain opacity={0.04} />\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/tile-wave.tsx"
    }
  ],
  "type": "registry:component"
}
