{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "bloom-ring",
  "title": "Bloom Ring",
  "description": "A ring of light blooms outward from the Remocn mark and replaces the scene.",
  "dependencies": [
    "remotion",
    "@remotion/google-fonts",
    "@remotion/transitions"
  ],
  "registryDependencies": ["@remocn/gsh"],
  "files": [
    {
      "path": "registry/remocn/bloom-ring/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadGeist } from \"@remotion/google-fonts/Geist\";\nimport { loadFont as loadSerif } from \"@remotion/google-fonts/InstrumentSerif\";\nimport { loadFont as loadManrope } from \"@remotion/google-fonts/Manrope\";\nimport {\n  linearTiming,\n  type TransitionPresentation,\n  type TransitionPresentationComponentProps,\n  TransitionSeries,\n} from \"@remotion/transitions\";\nimport type React from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  interpolateColors,\n  random,\n  spring,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\nimport type { GradientParams } from \"@/lib/remocn/gsh/gradient-engine\";\nimport { GshGround } from \"@/lib/remocn/gsh/gsh-ground\";\nimport type { GshPresetId } from \"@/lib/remocn/gsh/presets\";\n\nconst geist = loadGeist(\"normal\", {\n  weights: [\"300\", \"400\"],\n  subsets: [\"latin\"],\n});\nconst serif = loadSerif(\"italic\", { weights: [\"400\"], subsets: [\"latin\"] });\nconst manrope = loadManrope(\"normal\", {\n  weights: [\"600\"],\n  subsets: [\"latin\"],\n});\n\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\nexport type BloomRingProps = {\n  colors?: string[];\n  centerX?: number;\n  centerY?: number;\n  cell?: number;\n  stripe?: number;\n  seed?: string;\n};\n\nexport const bloomRingDefaults: Required<BloomRingProps> = {\n  colors: [\"#FFE7CF\", \"#FFB36B\", \"#F4842F\", \"#E2571C\"],\n  centerX: 0.5,\n  centerY: 0.5,\n  cell: 22,\n  stripe: 1.7,\n  seed: \"bloom\",\n};\n\nconst GROW = Easing.bezier(0.42, 0, 0.9, 0.62);\n\nfunction ringState(p: number) {\n  const radius = 0.03 + 1.4 * GROW(Math.min(1, Math.max(0, p)));\n  const width = 0.07 + radius * 0.3;\n  const heat =\n    interpolate(p, [0, 0.22], [0, 1], {\n      ...CLAMP,\n      easing: Easing.bezier(0.32, 0.72, 0, 1),\n    }) *\n    interpolate(p, [0.62, 1], [1, 0], {\n      ...CLAMP,\n      easing: Easing.bezier(0.55, 0, 1, 0.45),\n    });\n  return { radius, width, heat };\n}\n\nfunction Bloom({ p, props }: { p: number; props: Required<BloomRingProps> }) {\n  const { width: W, height: H } = useVideoConfig();\n  const { radius, width, heat } = ringState(p);\n  const cx = props.centerX * W;\n  const cy = props.centerY * H;\n  const reach = Math.hypot(Math.max(cx, W - cx), Math.max(cy, H - cy));\n  const ch = props.cell * (Math.min(W, H) / 720);\n  const cw = ch * props.stripe;\n  const cols = Math.ceil(W / cw) + 1;\n  const rows = Math.ceil(H / ch) + 1;\n  const R = radius * reach;\n  const Wd = width * reach;\n  const ramp = props.colors;\n  const rects: React.ReactNode[] = [];\n  for (let r = 0; r < rows; r++) {\n    const rowShift = (random(`${props.seed}-row-${r}`) - 0.5) * Wd * 0.26;\n    const rowPhase = random(`${props.seed}-ph-${r}`) * cw;\n    for (let c = -1; c < cols; c++) {\n      const x = c * cw - rowPhase;\n      const y = r * ch;\n      const d = Math.hypot(x + cw / 2 - cx, y + ch / 2 - cy);\n      const jitter = (random(`${props.seed}-${r}-${c}`) - 0.5) * Wd * 0.14;\n      const u = (d - R - rowShift - jitter) / Wd;\n      const inner = u < 0 ? Math.exp(-u * u * 3.2) : Math.exp(-u * u * 1.6);\n      const v = inner * heat;\n      if (v < 0.025) continue;\n      rects.push(\n        <rect\n          key={`${r}-${c}`}\n          x={x}\n          y={y}\n          width={cw + 0.6}\n          height={ch + 0.6}\n          fill={interpolateColors(\n            v,\n            [0, 0.45, 0.8, 1],\n            [ramp[1], ramp[2], ramp[3], ramp[2]],\n          )}\n          fillOpacity={Math.min(1, v ** 0.85 * 1.08)}\n        />,\n      );\n    }\n  }\n  return (\n    <svg\n      width={W}\n      height={H}\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        filter: `blur(${(ch * 0.42).toFixed(1)}px)`,\n      }}\n      aria-hidden\n    >\n      {rects}\n    </svg>\n  );\n}\n\nconst BloomRingPresentation: React.FC<\n  TransitionPresentationComponentProps<BloomRingProps>\n> = ({\n  children,\n  presentationProgress,\n  presentationDirection,\n  passedProps,\n}) => {\n  const props = { ...bloomRingDefaults, ...passedProps };\n  const { width: W, height: H } = useVideoConfig();\n  const p = presentationProgress;\n\n  if (presentationDirection === \"exiting\") {\n    const swell = interpolate(p, [0, 1], [1, 1.06], {\n      ...CLAMP,\n      easing: Easing.bezier(0.55, 0, 1, 0.45),\n    });\n    return (\n      <AbsoluteFill\n        style={{\n          transform: `scale(${swell})`,\n          opacity: interpolate(p, [0.24, 0.55], [1, 0.55], {\n            ...CLAMP,\n            easing: Easing.bezier(0.55, 0, 1, 0.45),\n          }),\n          filter: `blur(${interpolate(p, [0.18, 0.55], [0, 12], CLAMP).toFixed(2)}px)`,\n        }}\n      >\n        {children}\n      </AbsoluteFill>\n    );\n  }\n\n  const { radius, width } = ringState(p);\n  const cx = props.centerX * W;\n  const cy = props.centerY * H;\n  const reach = Math.hypot(Math.max(cx, W - cx), Math.max(cy, H - cy));\n  const hole = Math.max(0, (radius - width * 0.55) * reach);\n  const edge = hole + width * reach * 0.75;\n  const mask = `radial-gradient(circle at ${cx}px ${cy}px, #000 ${hole}px, transparent ${edge}px)`;\n  const settle = interpolate(p, [0.25, 1], [1.08, 1], {\n    ...CLAMP,\n    easing: Easing.bezier(0.22, 1, 0.36, 1),\n  });\n  return (\n    <AbsoluteFill>\n      <AbsoluteFill\n        style={{\n          maskImage: p >= 1 ? undefined : mask,\n          WebkitMaskImage: p >= 1 ? undefined : mask,\n          transform: `scale(${settle})`,\n        }}\n      >\n        {children}\n      </AbsoluteFill>\n      {p < 1 ? <Bloom p={p} props={props} /> : null}\n    </AbsoluteFill>\n  );\n};\n\nexport function bloomRing(\n  props: BloomRingProps = {},\n): TransitionPresentation<BloomRingProps> {\n  return { component: BloomRingPresentation, props };\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\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\ntype Theme = \"dark\" | \"light\";\n\nconst THEMES = {\n  dark: {\n    ink: \"#FFF4EA\",\n    muted: \"rgba(255,240,228,0.78)\",\n    accent: \"#FFB067\",\n    logo: \"#FFF1E4\",\n    groundA: \"ember\" as GshPresetId,\n    paramsA: undefined as Partial<GradientParams> | undefined,\n    groundB: \"hearth\" as GshPresetId,\n    paramsB: undefined as Partial<GradientParams> | undefined,\n    scrim: \"rgba(5,3,3,0.35)\",\n  },\n  light: {\n    ink: \"#1E1A16\",\n    muted: \"rgba(30,26,22,0.56)\",\n    accent: \"#E2662A\",\n    logo: \"#E8712A\",\n    groundA: \"paper-crease\" as GshPresetId,\n    paramsA: {\n      colors: [\"#ffd8b0\", \"#ffe9d4\", \"#f6efe6\", \"#e9b892\"],\n      background: \"#f6f2ec\",\n    } as Partial<GradientParams> | undefined,\n    groundB: \"corona\" as GshPresetId,\n    paramsB: {\n      kind: \"radial-glow\",\n      colors: [\"#ffffff\", \"#fff3e6\", \"#ffc590\", \"#f39a52\"],\n      background: \"#fdfaf6\",\n      grain: 0.1,\n    } as Partial<GradientParams> | undefined,\n    scrim: \"rgba(255,255,255,0)\",\n  },\n};\n\nconst BEATS = {\n  lineIn: -4,\n  wordGap: 3,\n  accentIn: 12,\n  logoIn: 13,\n  tagIn: 24,\n};\n\nfunction RiseWord({\n  children,\n  at,\n  style,\n}: {\n  children: React.ReactNode;\n  at: number;\n  style?: React.CSSProperties;\n}) {\n  const frame = useCurrentFrame();\n  const { fps } = useVideoConfig();\n  const s = spring({\n    frame: frame - at,\n    fps,\n    config: { mass: 0.8, damping: 18, stiffness: 160 },\n  });\n  return (\n    <span\n      style={{\n        display: \"inline-block\",\n        overflow: \"hidden\",\n        paddingBottom: \"0.14em\",\n        marginBottom: \"-0.14em\",\n        verticalAlign: \"bottom\",\n      }}\n    >\n      <span\n        style={{\n          display: \"inline-block\",\n          transform: `translateY(${(1 - s) * 105}%)`,\n          filter: `blur(${(Math.max(0, 1 - s) * 6).toFixed(2)}px)`,\n          ...style,\n        }}\n      >\n        {children}\n      </span>\n    </span>\n  );\n}\n\nfunction SceneLine({\n  theme,\n  lead,\n  accent,\n  ground,\n  groundParams,\n}: {\n  theme: Theme;\n  lead: string;\n  accent: string;\n  ground?: GshPresetId;\n  groundParams?: Partial<GradientParams>;\n}) {\n  const frame = useCurrentFrame();\n  const { width: W, height: H, durationInFrames } = useVideoConfig();\n  const t = THEMES[theme];\n  const tall = H > W;\n  const size = tall ? W * 0.09 : Math.min(W * 0.066, H * 0.12);\n  const push = interpolate(frame, [0, durationInFrames], [1, 1.035], CLAMP);\n  const words = lead.split(\" \");\n  const accentWords = accent.split(\" \");\n  return (\n    <AbsoluteFill>\n      <GshGround\n        preset={ground ?? t.groundA}\n        params={groundParams ?? t.paramsA}\n        timeOffset={4}\n        timeScale={0.5}\n      />\n      <AbsoluteFill style={{ backgroundColor: t.scrim }} />\n      <AbsoluteFill\n        style={{\n          justifyContent: \"center\",\n          alignItems: \"center\",\n          transform: `scale(${push})`,\n        }}\n      >\n        <div\n          style={{\n            width: tall ? W * 0.84 : W * 0.8,\n            textAlign: \"center\",\n            fontFamily: geist.fontFamily,\n            fontWeight: 300,\n            fontSize: size,\n            lineHeight: 1.08,\n            letterSpacing: \"-0.035em\",\n            color: t.ink,\n          }}\n        >\n          <div>\n            {words.map((w, i) => (\n              <RiseWord key={`${w}-${i}`} at={BEATS.lineIn + i * BEATS.wordGap}>\n                {w}\n                {\" \"}\n              </RiseWord>\n            ))}\n          </div>\n          <div>\n            {accentWords.map((w, i) => (\n              <RiseWord\n                key={`${w}-${i}`}\n                at={BEATS.accentIn + i * BEATS.wordGap}\n                style={{\n                  fontFamily: serif.fontFamily,\n                  fontStyle: \"italic\",\n                  fontWeight: 400,\n                  letterSpacing: \"-0.02em\",\n                  color: t.accent,\n                  fontSize: size * 1.08,\n                }}\n              >\n                {w}\n                {i < accentWords.length - 1 ? \" \" : \"\"}\n              </RiseWord>\n            ))}\n          </div>\n        </div>\n      </AbsoluteFill>\n    </AbsoluteFill>\n  );\n}\n\nfunction SceneLogo({\n  theme,\n  tagline,\n  ground,\n  groundParams,\n}: {\n  theme: Theme;\n  tagline: string;\n  ground?: GshPresetId;\n  groundParams?: Partial<GradientParams>;\n}) {\n  const frame = useCurrentFrame();\n  const { width: W, height: H, fps, durationInFrames } = useVideoConfig();\n  const t = THEMES[theme];\n  const tall = H > W;\n  const logoSize = tall ? W * 0.2 : Math.min(W * 0.11, H * 0.2);\n  const s = spring({\n    frame: frame - BEATS.logoIn,\n    fps,\n    config: { mass: 1, damping: 26, stiffness: 120 },\n  });\n  const tag = spring({\n    frame: frame - BEATS.tagIn,\n    fps,\n    config: { mass: 0.8, damping: 18, stiffness: 160 },\n  });\n  const push = interpolate(frame, [0, durationInFrames], [1, 1.03], CLAMP);\n  return (\n    <AbsoluteFill>\n      <GshGround\n        preset={ground ?? t.groundB}\n        params={groundParams ?? t.paramsB}\n        timeOffset={2}\n        timeScale={0.5}\n      />\n      <AbsoluteFill\n        style={{\n          justifyContent: \"center\",\n          alignItems: \"center\",\n          gap: logoSize * 0.34,\n          transform: `scale(${push})`,\n        }}\n      >\n        <div\n          style={{\n            transform: `translateY(${(1 - s) * logoSize * 0.18}px) scale(${0.94 + s * 0.06})`,\n            opacity: interpolate(s, [0, 0.5], [0.2, 1], CLAMP),\n            filter: `blur(${((1 - s) * 8).toFixed(2)}px)`,\n          }}\n        >\n          <RemocnLogo fontSize={logoSize} color={t.logo} />\n        </div>\n        <div\n          style={{\n            overflow: \"hidden\",\n            paddingBottom: \"0.12em\",\n          }}\n        >\n          <div\n            style={{\n              fontFamily: geist.fontFamily,\n              fontWeight: 300,\n              fontSize: logoSize * 0.27,\n              letterSpacing: \"-0.015em\",\n              color: t.muted,\n              transform: `translateY(${(1 - tag) * 110}%)`,\n              textAlign: \"center\",\n              maxWidth: W * 0.8,\n            }}\n          >\n            {tagline}\n          </div>\n        </div>\n      </AbsoluteFill>\n    </AbsoluteFill>\n  );\n}\n\nexport type BloomRingDemoProps = {\n  theme?: Theme;\n  lead?: string;\n  accentLine?: string;\n  tagline?: string;\n  ring?: BloomRingProps;\n  groundA?: GshPresetId;\n  groundAParams?: Partial<GradientParams>;\n  groundB?: GshPresetId;\n  groundBParams?: Partial<GradientParams>;\n  sceneA?: number;\n  sceneB?: number;\n  transition?: number;\n};\n\nexport const BLOOM_RING_DEMO = { sceneA: 72, sceneB: 84, transition: 30 };\n\nexport function BloomRingDemo({\n  theme = \"dark\",\n  lead = \"Your store goes live\",\n  accentLine = \"in fifteen minutes.\",\n  tagline = \"Launch films, built from blocks you own.\",\n  ring,\n  groundA,\n  groundAParams,\n  groundB,\n  groundBParams,\n  sceneA = BLOOM_RING_DEMO.sceneA,\n  sceneB = BLOOM_RING_DEMO.sceneB,\n  transition = BLOOM_RING_DEMO.transition,\n}: BloomRingDemoProps) {\n  return (\n    <AbsoluteFill\n      style={{ backgroundColor: theme === \"light\" ? \"#f6f2ec\" : \"#050303\" }}\n    >\n      <TransitionSeries>\n        <TransitionSeries.Sequence durationInFrames={sceneA}>\n          <SceneLine\n            theme={theme}\n            lead={lead}\n            accent={accentLine}\n            ground={groundA}\n            groundParams={groundAParams}\n          />\n        </TransitionSeries.Sequence>\n        <TransitionSeries.Transition\n          presentation={bloomRing(ring)}\n          timing={linearTiming({ durationInFrames: transition })}\n        />\n        <TransitionSeries.Sequence durationInFrames={sceneB}>\n          <SceneLogo\n            theme={theme}\n            tagline={tagline}\n            ground={groundB}\n            groundParams={groundBParams}\n          />\n        </TransitionSeries.Sequence>\n      </TransitionSeries>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/bloom-ring.tsx"
    }
  ],
  "type": "registry:component"
}
