{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "three-steps",
  "title": "Three Steps",
  "description": "Three large gradient numerals light along a connecting line: Install, Compose, Render.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/three-steps/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadSerif } from \"@remotion/google-fonts/InstrumentSerif\";\nimport { loadFont } from \"@remotion/google-fonts/Manrope\";\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: sans } = loadFont(\"normal\", {\n  weights: [\"400\", \"500\"],\n  subsets: [\"latin\"],\n});\nconst { fontFamily: serif } = loadSerif(\"italic\", {\n  weights: [\"400\"],\n  subsets: [\"latin\"],\n});\n\nexport interface Step {\n  title: string;\n  detail?: string;\n}\n\nexport interface ThreeStepsProps {\n  steps?: Step[];\n  eyebrow?: string;\n  outcome?: string;\n  accent?: string;\n  theme?: \"dark\" | \"light\";\n  speed?: number;\n}\n\nconst DEFAULT_STEPS: Step[] = [\n  { title: \"Install\", detail: \"Add a component with the shadcn CLI\" },\n  { title: \"Compose\", detail: \"Arrange scenes in plain React\" },\n  { title: \"Render\", detail: \"Export MP4 from Remotion\" },\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 EASE_LAND = Easing.bezier(0.2, 1.1, 0.36, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\nconst THEMES = {\n  dark: {\n    bg: \"#0A0A0B\",\n    key: \"rgba(255,255,255,0.05)\",\n    fg: \"#F3F2EF\",\n    muted: \"#8B8A90\",\n    hair: \"rgba(255,255,255,0.16)\",\n    // gradients.sh \"halo\": amber → rose → blue, read left to right across the steps.\n    ramp: [\"#ffb36b\", \"#e2457a\", \"#3d4dff\"],\n    grain: 0.06,\n  },\n  light: {\n    bg: \"#F5F4F0\",\n    key: \"rgba(255,255,255,0.85)\",\n    fg: \"#121214\",\n    muted: \"#6E6C67\",\n    hair: \"rgba(18,18,20,0.16)\",\n    // gradients.sh \"ribbon\" (light): orange → pink → violet.\n    ramp: [\"#ff7a2e\", \"#ff3d9a\", \"#5b3dff\"],\n    grain: 0.045,\n  },\n} as const;\n\ntype Theme = (typeof THEMES)[keyof typeof THEMES];\n\nfunction hexRgb(hex: string): [number, number, number] {\n  const n = Number.parseInt(hex.slice(1), 16);\n  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\n\n/** Sample an evenly spaced colour ramp at k ∈ [0, 1]. */\nfunction sampleRamp(ramp: readonly string[], k: number) {\n  const x = Math.min(1, Math.max(0, k)) * (ramp.length - 1);\n  const i = Math.min(ramp.length - 2, Math.floor(x));\n  const f = x - i;\n  const a = hexRgb(ramp[i]);\n  const b = hexRgb(ramp[i + 1]);\n  const m = a.map((v, j) => Math.round(v + (b[j] - v) * f));\n  return `rgb(${m[0]},${m[1]},${m[2]})`;\n}\n\nfunction rampCss(ramp: readonly string[]) {\n  return ramp\n    .map((col, i) => `${col} ${(i / (ramp.length - 1)) * 100}%`)\n    .join(\", \");\n}\n\nconst LINE_START = 2;\nconst LINE_DUR = 26;\nconst FIRST_FILL = 26;\nconst TRAVEL = 18;\nconst PAUSE = 10;\n\nexport const threeStepsLength = (count = 3) =>\n  FIRST_FILL + (count - 1) * (TRAVEL + PAUSE) + 30 + 40;\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={11}\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\nfunction rise(t: number, start: number, dur = 14) {\n  return interpolate(t, [start, start + dur], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n}\n\nfunction MaskLine({\n  t,\n  start,\n  children,\n  pad,\n  style,\n}: {\n  t: number;\n  start: number;\n  children: React.ReactNode;\n  pad: number;\n  style: React.CSSProperties;\n}) {\n  const e = interpolate(t, [start, start + 16], [0, 1], {\n    ...CLAMP,\n    easing: EASE_LAND,\n  });\n  const b = interpolate(t, [start, start + 7], [1, 0], CLAMP);\n  return (\n    <div style={{ overflow: \"hidden\", paddingBottom: pad }}>\n      <div\n        style={{\n          ...style,\n          transform: `translateY(${(1 - e) * 112}%)`,\n          filter: b > 0.02 ? `blur(${b * 4}px)` : undefined,\n          visibility: e > 0 ? \"visible\" : \"hidden\",\n        }}\n      >\n        {children}\n      </div>\n    </div>\n  );\n}\n\nfunction StepText({\n  index,\n  step,\n  t,\n  start,\n  ghostAt,\n  last,\n  outcome,\n  accent,\n  c,\n  u,\n  numSize,\n  sheen,\n}: {\n  index: number;\n  step: Step;\n  t: number;\n  start: number;\n  ghostAt: number;\n  last: boolean;\n  outcome?: string;\n  accent: string;\n  c: Theme;\n  u: number;\n  numSize: number;\n  /** Gradient band for the numeral: total width and this numeral's offset in it. */\n  sheen: { total: number; offset: number; width: number };\n}) {\n  const outIn = rise(t, start + 16, 18);\n  const lit = interpolate(t, [start - 2, start + 8], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const ruleIn = interpolate(t, [start + 12, start + 30], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        flexDirection: \"column\",\n        alignItems: \"center\",\n        textAlign: \"center\",\n      }}\n    >\n      <MaskLine\n        t={t}\n        start={ghostAt}\n        pad={numSize * 0.04}\n        style={{\n          fontFamily: sans,\n          fontWeight: 400,\n          fontSize: numSize,\n          lineHeight: 1,\n          letterSpacing: \"-0.04em\",\n          fontVariantNumeric: \"tabular-nums\",\n          color: `color-mix(in srgb, ${c.fg} 14%, ${c.bg})`,\n        }}\n      >\n        <span style={{ position: \"relative\", display: \"inline-block\" }}>\n          {String(index + 1).padStart(2, \"0\")}\n          <span\n            style={{\n              position: \"absolute\",\n              inset: 0,\n              backgroundImage: `linear-gradient(90deg, ${rampCss(c.ramp)})`,\n              backgroundSize: `${sheen.total}px 100%`,\n              backgroundPosition: `${-(sheen.offset - (sheen.width - numSize * 1.12) / 2)}px 0`,\n              WebkitBackgroundClip: \"text\",\n              backgroundClip: \"text\",\n              color: \"transparent\",\n              opacity: lit,\n            }}\n          >\n            {String(index + 1).padStart(2, \"0\")}\n          </span>\n        </span>\n      </MaskLine>\n      <div style={{ height: 18 * u }} />\n      <MaskLine\n        t={t}\n        start={start + 3}\n        pad={8 * u}\n        style={{\n          fontFamily: sans,\n          fontWeight: 500,\n          fontSize: 44 * u,\n          lineHeight: 1.05,\n          letterSpacing: \"-0.03em\",\n          color: c.fg,\n          whiteSpace: \"nowrap\",\n        }}\n      >\n        {step.title}\n      </MaskLine>\n      {step.detail ? (\n        <MaskLine\n          t={t}\n          start={start + 6}\n          pad={4 * u}\n          style={{\n            fontFamily: sans,\n            fontWeight: 400,\n            fontSize: 20 * u,\n            lineHeight: 1.35,\n            letterSpacing: \"-0.005em\",\n            color: c.muted,\n            textWrap: \"balance\",\n            maxWidth: 232 * u,\n            marginTop: 2 * u,\n          }}\n        >\n          {step.detail}\n        </MaskLine>\n      ) : null}\n      {last && outcome ? (\n        <div\n          style={{\n            marginTop: 22 * u,\n            display: \"flex\",\n            alignItems: \"center\",\n            gap: 12 * u,\n          }}\n        >\n          <div\n            style={{\n              width: 22 * u,\n              transform: `scaleX(${ruleIn})`,\n              opacity: ruleIn > 0.04 ? 1 : 0,\n              transformOrigin: \"right\",\n              height: Math.max(1.5, 2 * u),\n              borderRadius: u,\n              background: c.ramp[c.ramp.length - 1],\n            }}\n          />\n          <div\n            style={{\n              fontFamily: serif,\n              fontStyle: \"italic\",\n              fontWeight: 400,\n              fontSize: 28 * u,\n              lineHeight: 1,\n              letterSpacing: \"-0.005em\",\n              color: accent,\n              whiteSpace: \"nowrap\",\n              opacity: outIn,\n              transform: `translateX(${(1 - outIn) * -10 * u}px)`,\n              filter: `blur(${(1 - outIn) * 4 * u}px)`,\n            }}\n          >\n            {outcome}\n          </div>\n        </div>\n      ) : null}\n    </div>\n  );\n}\n\nexport function ThreeSteps({\n  steps = DEFAULT_STEPS,\n  eyebrow = \"How it works\",\n  outcome = \"Your launch video, shipped\",\n  accent = \"#3B5BFD\",\n  theme = \"dark\",\n  speed = 1,\n}: ThreeStepsProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H } = useVideoConfig();\n  const u = Math.min(W / 1280, H / 720);\n  const c = THEMES[theme];\n  const n = Math.max(1, steps.length);\n\n  const L = W * 0.08;\n  const colW = (W - 2 * L) / n;\n  const numSize = Math.min(146 * u, colW * 0.42);\n  const lineY = H * 0.5 - 146 * u;\n  const nodeR = 9 * u;\n  const nodeX = (i: number) => L + (i + 0.5) * colW;\n  const lastX = nodeX(n - 1);\n  const firstX = nodeX(0);\n\n  const fillAt = (i: number) => FIRST_FILL + i * (TRAVEL + PAUSE);\n  const done = fillAt(n - 1);\n\n  const lineP = interpolate(t, [LINE_START, LINE_START + LINE_DUR], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const lineEnd = firstX + (lastX - firstX) * lineP;\n\n  let dotX = firstX;\n  for (let i = 1; i < n; i++) {\n    const p = interpolate(t, [fillAt(i) - TRAVEL, fillAt(i)], [0, 1], {\n      ...CLAMP,\n      easing: EASE_IO,\n    });\n    dotX += (nodeX(i) - nodeX(i - 1)) * p;\n  }\n  const dotIn = interpolate(t, [FIRST_FILL - 8, FIRST_FILL], [0, 1], CLAMP);\n  const dotOut = interpolate(t, [done, done + 4], [1, 0], CLAMP);\n  const travelling = n > 1 && t > fillAt(0) && t < done;\n  const trail = travelling ? 1 : 0;\n\n  const eyeIn = rise(t, 8, 18);\n  const ringP = interpolate(t, [done, done + 26], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const cam = interpolate(t, [0, done + 60], [1, 1.025], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const stroke = Math.max(1, u);\n  const railId = `rail${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n\n  return (\n    <AbsoluteFill style={{ background: c.bg, overflow: \"hidden\" }}>\n      <AbsoluteFill\n        style={{\n          background: `radial-gradient(ellipse 60% 65% at 42% 40%, ${c.key}, transparent 72%)`,\n        }}\n      />\n      <AbsoluteFill\n        style={{ transform: `scale(${cam})`, transformOrigin: \"45% 45%\" }}\n      >\n        <div\n          style={{\n            position: \"absolute\",\n            left: 0,\n            right: 0,\n            textAlign: \"center\",\n            top: lineY - 70 * u,\n            fontFamily: sans,\n            fontWeight: 500,\n            fontSize: 15 * u,\n            lineHeight: 1,\n            letterSpacing: \"0.12em\",\n            textTransform: \"uppercase\",\n            color: c.muted,\n            opacity: eyeIn,\n            transform: `translateY(${(1 - eyeIn) * 8 * u}px)`,\n          }}\n        >\n          {eyebrow}\n        </div>\n\n        <svg\n          width={W}\n          height={H}\n          viewBox={`0 0 ${W} ${H}`}\n          style={{ position: \"absolute\", inset: 0, overflow: \"visible\" }}\n          aria-hidden\n        >\n          <line\n            x1={firstX}\n            y1={lineY}\n            x2={lineEnd}\n            y2={lineY}\n            stroke={c.hair}\n            strokeWidth={stroke}\n          />\n          <defs>\n            <linearGradient\n              id={railId}\n              gradientUnits=\"userSpaceOnUse\"\n              x1={firstX}\n              y1={0}\n              x2={lastX}\n              y2={0}\n            >\n              {c.ramp.map((col, i) => (\n                <stop\n                  key={col}\n                  offset={i / (c.ramp.length - 1)}\n                  stopColor={col}\n                />\n              ))}\n            </linearGradient>\n          </defs>\n          <line\n            x1={firstX}\n            y1={lineY}\n            x2={Math.max(firstX + 0.01, dotX)}\n            y2={lineY}\n            stroke={`url(#${railId})`}\n            strokeOpacity={dotIn}\n            strokeWidth={stroke * 2.5}\n            strokeLinecap=\"round\"\n          />\n          {steps.map((_, i) => {\n            const x = nodeX(i);\n            const reach = interpolate(\n              lineEnd,\n              [x - 30 * u, x + 4 * u],\n              [0, 1],\n              { ...CLAMP, easing: EASE_OUT },\n            );\n            const f = interpolate(t, [fillAt(i), fillAt(i) + 8], [0, 1], {\n              ...CLAMP,\n              easing: EASE_OUT,\n            });\n            const pop = Math.sin(Math.PI * f);\n            const fillColor = sampleRamp(\n              c.ramp,\n              (x - firstX) / Math.max(1, lastX - firstX),\n            );\n            const r = nodeR * (0.7 + 0.3 * reach) * (1 + 0.18 * pop);\n            return (\n              <g key={i} opacity={reach}>\n                <circle\n                  cx={x}\n                  cy={lineY}\n                  r={r}\n                  fill={c.bg}\n                  stroke={f > 0 ? fillColor : c.muted}\n                  strokeOpacity={f > 0 ? 1 : 0.7}\n                  strokeWidth={stroke * 1.5}\n                />\n                <circle\n                  cx={x}\n                  cy={lineY}\n                  r={r * f}\n                  fill={fillColor}\n                  opacity={f}\n                />\n              </g>\n            );\n          })}\n          {n > 0 ? (\n            <circle\n              cx={lastX}\n              cy={lineY}\n              r={nodeR * (1 + ringP * 2.6)}\n              fill=\"none\"\n              stroke={c.ramp[c.ramp.length - 1]}\n              strokeWidth={stroke * 1.5}\n              opacity={(1 - ringP) * (ringP > 0 ? 0.8 : 0)}\n            />\n          ) : null}\n          {[3, 2, 1].map((g) => (\n            <circle\n              key={g}\n              cx={dotX - g * 7 * u * trail}\n              cy={lineY}\n              r={nodeR * 0.55}\n              fill={c.fg}\n              opacity={dotIn * dotOut * trail * (0.22 / g)}\n            />\n          ))}\n          <circle\n            cx={dotX}\n            cy={lineY}\n            r={nodeR * 0.55}\n            fill={c.fg}\n            opacity={dotIn * dotOut}\n          />\n        </svg>\n\n        {steps.map((step, i) => (\n          <div\n            key={i}\n            style={{\n              position: \"absolute\",\n              left: nodeX(i) - colW / 2 + 12 * u,\n              top: lineY + 48 * u,\n              width: colW - 24 * u,\n            }}\n          >\n            <StepText\n              index={i}\n              step={step}\n              t={t}\n              start={fillAt(i) + 2}\n              ghostAt={\n                LINE_START + 4 + (i / Math.max(1, n - 1)) * LINE_DUR * 0.85\n              }\n              last={i === n - 1}\n              outcome={outcome}\n              accent={accent}\n              c={c}\n              u={u}\n              numSize={numSize}\n              sheen={{\n                total: lastX - firstX + numSize * 1.12,\n                offset: nodeX(i) - firstX,\n                width: numSize * 1.12,\n              }}\n            />\n          </div>\n        ))}\n      </AbsoluteFill>\n      <Grain opacity={c.grain} />\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/three-steps.tsx"
    }
  ],
  "type": "registry:component"
}
