{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tiny-planet",
  "title": "Tiny Planet",
  "description": "A seeded flat skyline bends into a tiny planet through a polar remap, then slowly spins beside a red sun.",
  "dependencies": ["remotion"],
  "files": [
    {
      "path": "registry/remocn/tiny-planet/index.tsx",
      "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  random,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nexport type TinyPlanetColors = {\n  sky: string;\n  ground: string;\n  near: string;\n  far: string;\n  window: string;\n  sun: string;\n};\n\nexport type TinyPlanetProps = {\n  /** Buildings in the front row; the back row gets a matching count. */\n  buildings?: number;\n  seed?: number;\n  /** Planet spin once bent, in degrees per frame at 30fps. */\n  spin?: number;\n  colors?: Partial<TinyPlanetColors>;\n  /** Sky color. Overrides `colors.sky`. */\n  skyColor?: string;\n  /** Sun color. Overrides `colors.sun`. */\n  sunColor?: string;\n  /** Front-row building color. Overrides `colors.near`. */\n  buildingColor?: string;\n  /** Ground color. Overrides `colors.ground`. */\n  groundColor?: string;\n  speed?: number;\n};\n\nexport const tinyPlanetDefaults = {\n  buildings: 26,\n  seed: 4,\n  spin: 0.7,\n  colors: {\n    sky: \"#111111\",\n    ground: \"#262626\",\n    near: \"#FFFFFF\",\n    far: \"#6B6B6B\",\n    window: \"#111111\",\n    sun: \"#E5322D\",\n  } satisfies TinyPlanetColors,\n  speed: 1,\n};\n\nconst BEND_START = 8;\nconst BEND_END = 40;\nconst SPIN_RAMP = 14;\n\ntype Block = { s0: number; s1: number; h: number; row: 0 | 1; spire: number };\n\nfunction makeCity(length: number, height: number, count: number, seed: number) {\n  const blocks: Block[] = [];\n  for (const row of [1, 0] as const) {\n    const n = row === 1 ? Math.round(count * 0.8) : count;\n    const slot = length / n;\n    for (let i = 0; i < n; i++) {\n      const key = `tiny-planet-${seed}-${row}-${i}`;\n      const w = slot * (0.55 + random(`${key}-w`) * 0.4);\n      const start =\n        i * slot + random(`${key}-x`) * (slot - w) + (row ? slot * 0.5 : 0);\n      const tall = random(`${key}-h`);\n      const h = row\n        ? height * (0.13 + tall * 0.16)\n        : height * (0.05 + tall * tall * 0.15);\n      const spire = random(`${key}-s`) < 0.18 ? height * 0.04 : 0;\n      blocks.push({\n        s0: start - length / 2,\n        s1: start + w - length / 2,\n        h,\n        row,\n        spire,\n      });\n    }\n  }\n  return blocks;\n}\n\nexport function TinyPlanet({\n  buildings = tinyPlanetDefaults.buildings,\n  seed = tinyPlanetDefaults.seed,\n  spin = tinyPlanetDefaults.spin,\n  colors: colorOverrides,\n  skyColor,\n  sunColor,\n  buildingColor,\n  groundColor,\n  speed = tinyPlanetDefaults.speed,\n}: TinyPlanetProps) {\n  const frame = useCurrentFrame();\n  const { width, height, fps } = useVideoConfig();\n  const baseColors = { ...tinyPlanetDefaults.colors, ...colorOverrides };\n  const colors = {\n    ...baseColors,\n    sky: skyColor ?? baseColors.sky,\n    sun: sunColor ?? baseColors.sun,\n    near: buildingColor ?? baseColors.near,\n    ground: groundColor ?? baseColors.ground,\n  };\n  const t = (frame * speed * 30) / fps;\n\n  // The skyline strip is exactly as long as the planet's circumference.\n  const length = width;\n  const radius = length / (2 * Math.PI);\n  const city = useMemo(\n    () => makeCity(length, height, Math.max(4, Math.round(buildings)), seed),\n    [length, height, buildings, seed],\n  );\n\n  const bend = interpolate(t, [BEND_START, BEND_END], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: Easing.inOut(Easing.cubic),\n  });\n  // Spin eases in while the strip closes, then turns at a steady rate.\n  const spinT = Math.max(0, t - (BEND_END - SPIN_RAMP));\n  const turn =\n    spinT < SPIN_RAMP\n      ? (spin * spinT * spinT) / (2 * SPIN_RAMP)\n      : spin * (spinT - SPIN_RAMP / 2);\n  const rot = (turn * Math.PI) / 180;\n\n  const kappa = Math.max(1e-5, bend / radius);\n  const horizon = height * 0.7;\n  const tallest = height * 0.33;\n  const finalScale = (height * 0.43) / (radius + tallest);\n  const scale = 1 + (finalScale - 1) * bend;\n  const anchorY = horizon + (height / 2 - finalScale * radius - horizon) * bend;\n\n  // Bend: arc-length s and height h onto a circle of curvature kappa whose\n  // top sits at the pivot. kappa -> 0 is the flat skyline.\n  const map = (s: number, h: number): [number, number] => {\n    const a = kappa * s + rot * bend;\n    const rr = 1 / kappa + h;\n    const x = Math.sin(a) * rr;\n    const y = 1 / kappa - Math.cos(a) * rr;\n    return [width / 2 + x * scale, anchorY + y * scale];\n  };\n  const P = (s: number, h: number) =>\n    map(s, h)\n      .map((v) => v.toFixed(1))\n      .join(\",\");\n\n  const steps = 160;\n  const depth = Math.min(1 / kappa, height * 3);\n  let ground = \"\";\n  for (let i = 0; i <= steps; i++) {\n    const s = -length / 2 + (length * i) / steps;\n    ground += `${i ? \"L\" : \"M\"}${P(s, 0)}`;\n  }\n  for (let i = steps; i >= 0; i--) {\n    const s = -length / 2 + (length * i) / steps;\n    ground += `L${P(s, -depth)}`;\n  }\n\n  const sunX = interpolate(bend, [0, 1], [width * 0.7, width * 0.8]);\n  const sunY = interpolate(bend, [0, 1], [height * 0.36, height * 0.2]);\n  const sunR = height * interpolate(bend, [0, 1], [0.12, 0.075]);\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: colors.sky, overflow: \"hidden\" }}>\n      <svg\n        width={width}\n        height={height}\n        viewBox={`0 0 ${width} ${height}`}\n        style={{ position: \"absolute\", inset: 0 }}\n        aria-hidden\n      >\n        <circle cx={sunX} cy={sunY} r={sunR} fill={colors.sun} />\n        {city.map((b, i) => {\n          const sub = 4;\n          const top: string[] = [];\n          for (let k = 0; k <= sub; k++)\n            top.push(P(b.s0 + ((b.s1 - b.s0) * k) / sub, b.h));\n          const mid = (b.s0 + b.s1) / 2;\n          const spire = b.spire\n            ? ` M${P(mid - 1.5, b.h)} L${P(mid, b.h + b.spire)} L${P(mid + 1.5, b.h)}`\n            : \"\";\n          const d = `M${P(b.s0, -2)} L${top.join(\" L\")} L${P(b.s1, -2)} Z${spire}`;\n          return (\n            <path\n              key={i}\n              d={d}\n              fill={b.row ? colors.far : colors.near}\n              stroke={b.row ? colors.far : colors.near}\n              strokeWidth={b.spire ? 2 * scale : 0}\n            />\n          );\n        })}\n        {city.map((b, i) => {\n          if (b.row || b.h < height * 0.09) return null;\n          const cols = Math.max(1, Math.floor((b.s1 - b.s0) / (height * 0.03)));\n          const rows = Math.floor(b.h / (height * 0.04));\n          const cells: string[] = [];\n          for (let r = 0; r < rows; r++) {\n            for (let c = 0; c < cols; c++) {\n              if (random(`tiny-planet-${seed}-win-${i}-${r}-${c}`) < 0.55)\n                continue;\n              const s = b.s0 + ((b.s1 - b.s0) * (c + 0.5)) / cols;\n              const h = b.h - height * 0.03 - r * height * 0.04;\n              const ws = height * 0.006;\n              cells.push(\n                `M${P(s - ws, h)} L${P(s + ws, h)} L${P(s + ws, h - ws * 2.2)} L${P(s - ws, h - ws * 2.2)} Z`,\n              );\n            }\n          }\n          return (\n            <path key={`w${i}`} d={cells.join(\" \")} fill={colors.window} />\n          );\n        })}\n        <path d={ground} fill={colors.ground} />\n      </svg>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/tiny-planet.tsx"
    }
  ],
  "type": "registry:component"
}
