{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "gravity-grid",
  "title": "Gravity Grid",
  "description": "A grid bends into a gravity well and the logo lands at its bottom.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/gravity-grid/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont } from \"@remotion/google-fonts/HankenGrotesk\";\nimport { loadFont as loadManrope } from \"@remotion/google-fonts/Manrope\";\nimport type React from \"react\";\nimport { useId } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  interpolateColors,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst { fontFamily: sans } = loadFont(\"normal\", {\n  weights: [\"400\", \"500\"],\n  subsets: [\"latin\"],\n});\nconst manrope = loadManrope(\"normal\", {\n  weights: [\"500\", \"600\"],\n  subsets: [\"latin\"],\n});\n\nconst REMOCN_R_PATH =\n  \"M 0.01 0.81 C 0.01 1.73, 0.36 2.79, 1.09 4.13 C 4.91 11.04, 13.45 17.16, 21.7 18.9 C 22.94 19.16, 23.18 19.16, 51.39 19.27 C 76.07 19.36, 80.02 19.4, 81.32 19.57 C 89.89 20.69, 96.2 24.68, 99.38 31.01 C 103.19 38.56, 102.53 50.1, 97.91 57.07 C 94.66 61.96, 89.68 64.99, 83.26 66 C 82.81 66.07, 70.18 66.83, 55.2 67.69 C 24.82 69.43, 27.03 69.23, 24.18 70.4 C 19.9 72.15, 14.84 75.7, 10.65 79.89 C 4.86 85.68, 1.3 91.91, 0.25 98.13 C 0.12 98.85, 0.08 103.13, 0.04 116.66 L 0 134.26 9.5 134.26 L 19 134.26 19.05 119.41 C 19.1 103.27, 19.08 103.82, 19.82 101.04 C 21.79 93.65, 27.86 88.75, 36.45 87.63 C 37.23 87.53, 39.41 87.5, 43.57 87.53 C 50.12 87.59, 50.75 87.65, 53.22 88.45 C 56.61 89.56, 59.67 91.86, 62.02 95.07 C 62.52 95.76, 69.35 103.85, 77.2 113.07 C 85.04 122.28, 91.63 130.04, 91.85 130.32 C 92.07 130.59, 92.5 131.34, 92.82 131.97 C 93.52 133.35, 94.11 133.93, 95.06 134.13 C 95.5 134.23, 98.97 134.26, 106.36 134.23 L 117.01 134.19 100.82 113.07 C 91.91 101.45, 84.52 91.78, 84.39 91.57 C 83.36 89.89, 83.66 87.53, 85.09 86 C 85.79 85.25, 86.36 84.94, 88.07 84.38 C 96.18 81.72, 104.15 76.62, 109.97 70.36 C 120.59 58.93, 124.06 44.32, 119.44 30.43 C 114.59 15.81, 101.93 4.02, 87.64 0.83 C 83.93 0.01, 88.09 0.08, 41.85 0.04 L 0.01 0 0.01 0.81 Z\";\n\n/** Full logo as on remocn.dev: R mark (0.76em tall, baseline-aligned) + \"emocn\" in Manrope 600. */\nfunction RemocnLogo({ fontSize, color }: { fontSize: number; color: string }) {\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        alignItems: \"baseline\",\n        color,\n        fontFamily: manrope.fontFamily,\n        fontWeight: 600,\n        fontSize,\n        letterSpacing: \"-0.025em\",\n        lineHeight: 1,\n      }}\n    >\n      <svg\n        viewBox=\"0 0 124.06 134.26\"\n        style={{ height: \"0.76em\", width: \"auto\" }}\n        aria-hidden\n      >\n        <path fill=\"currentColor\" d={REMOCN_R_PATH} />\n      </svg>\n      <span style={{ marginLeft: \"0.04em\" }}>emocn</span>\n    </div>\n  );\n}\n\nexport interface GravityGridProps {\n  /** Replaces the remocn logo at the bottom of the well; `name` is set beside it. */\n  logo?: React.ReactNode;\n  name?: string;\n  /** Small line under the name. Pass an empty string to hide it. */\n  caption?: string;\n  accent?: string;\n  /** Playback multiplier. */\n  speed?: number;\n  theme?: \"dark\" | \"light\";\n}\n\nconst EASE_IO = Easing.bezier(0.65, 0, 0.35, 1);\nconst EASE_OUT = Easing.bezier(0.22, 1, 0.36, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\n// gradients.sh \"radial-glow\" palette: bg #020204, [#f3f1ff, #9db4ff, #5a46cb, #1d1240].\n// It lights the well: a glowing ring at the bottom, and the brightest grid lines.\nconst GLOW = [\"#f3f1ff\", \"#9db4ff\", \"#5a46cb\", \"#1d1240\"] as const;\n\nconst THEMES = {\n  dark: {\n    bg: \"#020204\",\n    ramp: [\"#8E8D99\", \"#8E8D99\", GLOW[1], GLOW[0]],\n    fg: \"#F3F2EF\",\n    muted: \"#8B8A90\",\n    base: 0.13,\n    lit: 0.95,\n    pool: \"rgba(2,2,4,0.9)\",\n    ring: [0.75, 0.5, 0.42],\n    disc: 0.55,\n    vignette: \"rgba(0,0,0,0.65)\",\n    grain: 0.06,\n    glow: 0.32,\n    bloom: 0.6,\n  },\n  light: {\n    bg: \"#F5F4F0\",\n    ramp: [\"#55545C\", \"#55545C\", GLOW[2], GLOW[3]],\n    fg: \"#121214\",\n    muted: \"#6E6C67\",\n    base: 0.12,\n    lit: 0.8,\n    pool: \"rgba(245,244,240,0.94)\",\n    ring: [0.0, 0.7, 0.5],\n    disc: 0.42,\n    vignette: \"rgba(120,110,95,0.14)\",\n    grain: 0.045,\n    glow: 0.14,\n    bloom: 0,\n  },\n} as const;\n\n/** Frame the mass reaches the bottom of the well. */\nconst IMPACT = 72;\nconst WELL_Z = 13;\nconst CORE = 2.6;\nconst DEPTH = 5.4;\nconst LEVELS = 14;\n\nfunction smooth(a: number, b: number, x: number) {\n  const k = Math.min(1, Math.max(0, (x - a) / (b - a)));\n  return k * k * (3 - 2 * k);\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 }}\n      aria-hidden\n    >\n      <filter id={id}>\n        <feTurbulence\n          type=\"fractalNoise\"\n          baseFrequency=\"0.85\"\n          numOctaves={2}\n          seed={5}\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 GravityGrid({\n  logo,\n  name = \"remocn\",\n  caption = \"Launch videos, in components.\",\n  accent = \"#3B5BFD\",\n  speed = 1,\n  theme = \"dark\",\n}: GravityGridProps) {\n  const t = useCurrentFrame() * speed;\n  const { width, height } = useVideoConfig();\n  const u = height / 720;\n  const pal = THEMES[theme];\n  const id = useId().replace(/[^a-zA-Z0-9_-]/g, \"\");\n\n  // ---- the mass: accelerates down, lands, the well overshoots and settles ----\n  const fallP = interpolate(t, [14, IMPACT], [0, 1], CLAMP);\n  const fall = fallP ** 2.2;\n  const after = Math.max(0, t - IMPACT);\n  const bounce =\n    t > IMPACT ? 0.12 * Math.exp(-after / 9) * Math.sin(after / 3.2) : 0;\n  const depth = DEPTH * (fall + bounce);\n  const massY = interpolate(fall, [0, 1], [7, -DEPTH + 0.9]);\n\n  // ---- one ripple from the impact ----\n  const ripR = after * 0.42;\n  const ripAmp = t > IMPACT ? 0.55 * Math.exp(-after / 34) : 0;\n  const rippleAt = (r: number) => {\n    if (ripAmp <= 0.001) return 0;\n    const dr = r - ripR;\n    return (\n      ripAmp *\n      Math.exp(-(dr * dr) / 5) *\n      Math.sin(dr * 1.5) *\n      smooth(0, 2.5, ripR - 1.5)\n    );\n  };\n\n  const heightAt = (x: number, z: number) => {\n    const dx = x;\n    const dz = z - WELL_Z;\n    const r = Math.sqrt(dx * dx + dz * dz);\n    return -depth * (CORE / Math.sqrt(r * r + CORE * CORE)) + rippleAt(r);\n  };\n\n  // ---- camera: slow push in, tilting down to follow the mass ----\n  const follow = interpolate(t, [10, IMPACT + 18], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const camH = 6.4 - follow * 0.3;\n  const camZ = interpolate(t, [0, 240], [-3.4, -2], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const f = height * 1.3;\n  // look at the rim first, then down into the well as the mass sinks\n  const lookY = -DEPTH * 0.92 * follow;\n  const pitch =\n    Math.atan2(camH - lookY, WELL_Z - camZ) -\n    Math.atan((0.03 * height) / f) * (1 - follow);\n  const camX = Math.sin(t / 90) * 0.35;\n  const cosP = Math.cos(pitch);\n  const sinP = Math.sin(pitch);\n  const project = (x: number, y: number, z: number) => {\n    const dx = x - camX;\n    const dy = y - camH;\n    const dz = z - camZ;\n    const yc = dy * cosP + dz * sinP;\n    const zc = -dy * sinP + dz * cosP;\n    return {\n      x: width / 2 + (f * dx) / zc,\n      y: height / 2 - (f * yc) / zc,\n      zc,\n    };\n  };\n\n  // ---- lockup at the bottom of the well ----\n  const lockIn = interpolate(t, [IMPACT + 6, IMPACT + 32], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const capIn = interpolate(t, [IMPACT + 18, IMPACT + 40], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n\n  // ---- light from the mass catches the curvature ----\n  const brightness = (x: number, z: number, y: number) => {\n    const e = 0.05;\n    const gx = (heightAt(x + e, z) - heightAt(x - e, z)) / (2 * e);\n    const gz = (heightAt(x, z + e) - heightAt(x, z - e)) / (2 * e);\n    const nl = Math.sqrt(gx * gx + 1 + gz * gz);\n    const lx = -x;\n    const ly = massY - y;\n    const lz = WELL_Z - z;\n    const d2 = lx * lx + ly * ly + lz * lz;\n    const dl = Math.sqrt(d2);\n    const ndl = Math.max(0, (-gx * lx + ly - gz * lz) / (nl * dl));\n    const falloff = 14 / (14 + d2);\n    // the slope itself catches a rim of light, even far from the mass\n    const slope = Math.min(1, Math.sqrt(gx * gx + gz * gz) * 0.9);\n    const r = Math.sqrt(x * x + (z - WELL_Z) ** 2);\n    const rip =\n      ripAmp > 0 ? Math.exp(-((r - ripR) ** 2) / 4) * ripAmp * 1.1 : 0;\n    return ndl * falloff * 1.6 + slope * 0.55 * fall + rip;\n  };\n\n  const levels: string[] = Array.from({ length: LEVELS + 1 }, () => \"\");\n  const addLine = (pts: { x: number; z: number }[]) => {\n    let prev: { sx: number; sy: number; ok: boolean } | null = null;\n    for (const p of pts) {\n      const y = heightAt(p.x, p.z);\n      const s = project(p.x, y, p.z);\n      const ok = s.zc > 0.6;\n      if (prev?.ok && ok) {\n        const r = Math.sqrt(p.x * p.x + (p.z - WELL_Z) ** 2);\n        const fog =\n          (1 - smooth(18, 80, s.zc)) *\n          smooth(0.6, 3, s.zc) *\n          (1 - smooth(16, 34, Math.abs(p.x)));\n        const hole = 1 - lockIn * (1 - smooth(0.3, 2, r)) * 0.8;\n        const b =\n          (pal.base + pal.lit * Math.min(1, brightness(p.x, p.z, y))) *\n          fog *\n          hole;\n        const lv = Math.min(LEVELS, Math.round(b * LEVELS));\n        if (lv > 0) {\n          levels[lv] +=\n            `M${prev.sx.toFixed(1)} ${prev.sy.toFixed(1)}L${s.x.toFixed(1)} ${s.y.toFixed(1)}`;\n        }\n      }\n      prev = { sx: s.x, sy: s.y, ok };\n    }\n  };\n\n  const STEP = 0.5;\n  for (let gx = -34; gx <= 34; gx++) {\n    const pts = [];\n    for (let z = 1; z <= 90; z += z < 30 ? STEP : 1) pts.push({ x: gx, z });\n    addLine(pts);\n  }\n  for (let gz = 1; gz <= 90; gz++) {\n    const pts = [];\n    for (let x = -34; x <= 34; x += Math.abs(x) < 14 ? STEP : 1)\n      pts.push({ x, z: gz });\n    addLine(pts);\n  }\n\n  const bottom = project(0, -depth, WELL_Z);\n  const horizonY = height / 2 - f * Math.tan(pitch);\n  const scaleAtBottom = f / bottom.zc / (height * 0.072);\n  const lift = (1 - lockIn) * 14 * u;\n  const markSize = 62 * u * scaleAtBottom;\n  const glowR = height * 0.34;\n  const ringRx = height * 0.42 * (0.9 + 0.1 * lockIn) * (1 + ripAmp * 0.25);\n  const ringRy = ringRx * Math.max(0.2, Math.sin(pitch) * 0.62);\n  const massS = project(0, Math.max(massY, -depth + 0.4), WELL_Z);\n  const massR = (6 * u * f) / Math.max(1, massS.zc) / (height * 0.072);\n  const massO =\n    interpolate(t, [10, 22], [0, 1], CLAMP) *\n    (1 - interpolate(t, [IMPACT - 2, IMPACT + 10], [0, 1], CLAMP));\n  const mixA = (c: string, a: number) =>\n    `color-mix(in oklab, ${c} ${Math.round(Math.max(0, Math.min(1, a)) * 100)}%, transparent)`;\n  const glowO = pal.glow * (0.35 * fall + 0.65 * lockIn) * (1 + ripAmp * 0.6);\n\n  return (\n    <AbsoluteFill style={{ background: pal.bg, overflow: \"hidden\" }}>\n      {/* the light at the bottom of the well */}\n      <AbsoluteFill\n        style={{\n          background: [\n            `radial-gradient(${ringRx}px ${ringRy}px at ${bottom.x}px ${bottom.y + ringRy * 0.12}px, transparent 46%, ${mixA(GLOW[0], pal.ring[0])} 55%, ${mixA(GLOW[1], pal.ring[1])} 62%, ${mixA(GLOW[2], pal.ring[2])} 70%, transparent 86%)`,\n            `radial-gradient(circle ${glowR}px at ${bottom.x}px ${bottom.y}px, ${mixA(GLOW[2], pal.disc)}, ${mixA(GLOW[3], pal.disc * 0.6)} 40%, transparent 70%)`,\n          ].join(\", \"),\n          opacity: glowO,\n        }}\n      />\n      <svg\n        width={width}\n        height={height}\n        style={{ position: \"absolute\", inset: 0 }}\n        aria-hidden\n      >\n        <defs>\n          <filter id={`bloom${id}`}>\n            <feGaussianBlur stdDeviation={2.2 * u} />\n          </filter>\n        </defs>\n        {/* a soft bloom under the brightest lines */}\n        <g filter={`url(#bloom${id})`} opacity={pal.bloom}>\n          {levels.map((d, lv) =>\n            lv >= LEVELS * 0.55 && d ? (\n              <path\n                key={lv}\n                d={d}\n                stroke={GLOW[2]}\n                strokeWidth={2.4 * u}\n                fill=\"none\"\n                opacity={(lv / LEVELS) ** 2}\n              />\n            ) : null,\n          )}\n        </g>\n        {levels.map((d, lv) =>\n          d ? (\n            <path\n              key={lv}\n              d={d}\n              stroke={interpolateColors(\n                lv / LEVELS,\n                [0, 0.42, 0.72, 1],\n                [...pal.ramp],\n              )}\n              strokeOpacity={lv / LEVELS}\n              strokeWidth={1.05 * u}\n              strokeLinecap=\"round\"\n              fill=\"none\"\n            />\n          ) : null,\n        )}\n      </svg>\n\n      {/* the mass: a bright accent bead that sinks and becomes the light */}\n      <div\n        style={{\n          position: \"absolute\",\n          left: massS.x - massR,\n          top: massS.y - massR,\n          width: massR * 2,\n          height: massR * 2,\n          borderRadius: \"50%\",\n          background: accent,\n          boxShadow: `0 0 ${massR * 3}px ${massR}px ${mixA(accent, 0.45)}`,\n          opacity: massO,\n        }}\n      />\n\n      {/* the name settles where the mass came to rest */}\n      <div\n        style={{\n          position: \"absolute\",\n          left: bottom.x,\n          top: bottom.y,\n          transform: `translate(-50%, -50%) translateY(${lift}px)`,\n          display: \"flex\",\n          flexDirection: \"column\",\n          alignItems: \"center\",\n          gap: 14 * u * scaleAtBottom,\n        }}\n      >\n        <div\n          style={{\n            position: \"absolute\",\n            left: \"50%\",\n            top: \"50%\",\n            width: markSize * 6.2,\n            height: markSize * 3.4,\n            transform: \"translate(-50%, -50%)\",\n            background: `radial-gradient(ellipse closest-side, ${pal.pool}, transparent)`,\n            opacity: lockIn * 0.95,\n          }}\n        />\n        <div\n          style={{\n            position: \"relative\",\n            display: \"flex\",\n            alignItems: \"center\",\n            gap: 10 * u * scaleAtBottom,\n            opacity: lockIn,\n            filter: `blur(${(1 - lockIn) * 6 * u}px)`,\n            transform: `scale(${0.94 + lockIn * 0.06})`,\n          }}\n        >\n          {logo ? (\n            <>\n              {logo}\n              <div\n                style={{\n                  fontFamily: sans,\n                  fontWeight: 500,\n                  fontSize: markSize * 0.95,\n                  lineHeight: 1,\n                  letterSpacing: \"-0.035em\",\n                  color: pal.fg,\n                }}\n              >\n                {name}\n              </div>\n            </>\n          ) : (\n            <RemocnLogo fontSize={markSize * 1.02} color={pal.fg} />\n          )}\n        </div>\n        {caption ? (\n          <div\n            style={{\n              position: \"relative\",\n              fontFamily: sans,\n              fontWeight: 400,\n              fontSize: 18 * u * scaleAtBottom,\n              letterSpacing: \"0em\",\n              color: pal.muted,\n              opacity: capIn,\n              transform: `translateY(${(1 - capIn) * 8 * u}px)`,\n              whiteSpace: \"nowrap\",\n            }}\n          >\n            {caption}\n          </div>\n        ) : null}\n      </div>\n\n      {/* atmospheric falloff toward the horizon */}\n      <AbsoluteFill\n        style={{\n          background: `linear-gradient(to bottom, ${pal.bg} ${Math.max(0, horizonY + height * 0.03)}px, ${pal.bg}00 ${horizonY + height * 0.38}px)`,\n        }}\n      />\n      <AbsoluteFill\n        style={{\n          background: `radial-gradient(ellipse 80% 75% at 50% 50%, transparent 50%, ${pal.vignette} 100%)`,\n        }}\n      />\n      <Grain opacity={pal.grain} />\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/gravity-grid.tsx"
    }
  ],
  "type": "registry:component"
}
