{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "query-cascade",
  "title": "Query Cascade",
  "description": "Search queries rush past until the hero query lands and its answer line resolves.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "registryDependencies": ["@remocn/gsh"],
  "files": [
    {
      "path": "registry/remocn/query-cascade/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadManrope } from \"@remotion/google-fonts/Manrope\";\nimport { loadFont } from \"@remotion/google-fonts/SchibstedGrotesk\";\nimport type React from \"react\";\nimport { useMemo } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  interpolateColors,\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 { fontFamily: grotesk } = loadFont(\"normal\", {\n  weights: [\"400\"],\n  subsets: [\"latin\"],\n});\nconst manrope = loadManrope(\"normal\", {\n  weights: [\"600\"],\n  subsets: [\"latin\"],\n});\n\nexport interface QueryCascadeProps {\n  queries?: string[];\n  hero?: string;\n  term?: string;\n  answerBefore?: string;\n  answerAfter?: string;\n  showLogo?: boolean;\n  accent?: string;\n  theme?: \"dark\" | \"light\";\n  ground?: GshPresetId;\n  groundParams?: Partial<GradientParams>;\n  speed?: number;\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\";\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}\nconst ADV = [\n  254, 240, 353, 654, 564, 850, 686, 199, 309, 309, 443, 635, 265, 488, 280,\n  444, 611, 343, 572, 576, 584, 580, 595, 531, 579, 594, 280, 268, 635, 635,\n  635, 529, 885, 700, 645, 703, 697, 585, 561, 718, 720, 354, 536, 660, 545,\n  868, 729, 770, 615, 770, 633, 613, 644, 673, 678, 935, 652, 623, 595, 310,\n  444, 310, 635, 500, 500, 551, 589, 551, 589, 573, 341, 555, 567, 242, 272,\n  552, 262, 873, 581, 597, 590, 590, 401, 513, 340, 581, 554, 812, 559, 548,\n  504, 327, 239, 327, 635,\n];\nconst HERO_TRACK = -0.025;\nconst BASELINE = 0.8594;\n\nconst QUERIES = [\n  \"launch video ideas\",\n  \"how long should a launch video be\",\n  \"launch video in a day\",\n  \"product demo video template\",\n  \"best saas launch videos\",\n  \"how to animate text in react\",\n  \"launch video music no copyright\",\n  \"make a demo video fast\",\n  \"motion design for developers\",\n  \"how to cut a video to the beat\",\n  \"product hunt launch video\",\n  \"launch trailer for an app\",\n  \"motion graphics without after effects\",\n  \"how to make a product video\",\n  \"launch video script\",\n  \"animated logo reveal\",\n  \"how to record a screen demo\",\n  \"launch video for developers\",\n  \"kinetic typography in react\",\n  \"launch video checklist\",\n  \"how to make an explainer video\",\n  \"best launch videos 2026\",\n  \"launch video examples\",\n  \"react video components\",\n  \"how to make a teaser video\",\n  \"launch video before friday\",\n  \"video templates for startups\",\n  \"how to make a launch film\",\n  \"launch video in an afternoon\",\n  \"how to edit a launch video\",\n  \"product launch video cost\",\n  \"launch video with code\",\n  \"how to make a launch video fast\",\n  \"launch video templates free\",\n  \"how to make a launch video in react\",\n  \"launch video for a startup\",\n  \"launch video length\",\n  \"launch video agency\",\n  \"how to make a launch video with ai\",\n];\nconst HERO_FROM_END = 6;\nconst LIGHT_GROUND: Partial<GradientParams> = {\n  kind: \"mist\",\n  colors: [\"#EE8550\", \"#F5C29C\", \"#F2E4D6\", \"#5E1E12\"],\n  background: \"#F2EEE8\",\n  speed: 0.8,\n  loopSeconds: 14,\n  scale: 0.8,\n  warp: 0.4,\n  blur: 0.65,\n  grain: 0.3,\n  seed: 6,\n};\n\nconst THEMES = {\n  dark: {\n    base: \"#0A0302\",\n    scrim:\n      \"radial-gradient(70% 55% at 50% 46%, rgba(10,3,2,0.78) 0%, rgba(10,3,2,0.5) 70%, rgba(10,3,2,0.3) 100%)\",\n    fg: \"#F4F1EC\",\n    term: \"#FFB27A\",\n    field: \"rgba(20,14,12,0.92)\",\n    fieldDepth:\n      \"inset 0 1px 0 rgba(255,255,255,0.06), 0 0 0 1px rgba(255,255,255,0.09), 0 24px 50px -18px rgba(0,0,0,0.8)\",\n    icon: \"#8F8780\",\n    keyRing: \"rgba(255,255,255,0.14)\",\n  },\n  light: {\n    base: \"#F2EEE8\",\n    scrim:\n      \"radial-gradient(70% 55% at 50% 46%, rgba(246,243,238,0.72) 0%, rgba(246,243,238,0.3) 100%)\",\n    fg: \"#17120F\",\n    term: \"#C2410C\",\n    field: \"rgba(255,255,255,0.95)\",\n    fieldDepth:\n      \"0 0 0 1px rgba(40,24,12,0.07), 0 1px 3px rgba(40,24,12,0.05), 0 24px 50px -20px rgba(90,40,10,0.25)\",\n    icon: \"#857B73\",\n    keyRing: \"rgba(40,24,12,0.16)\",\n  },\n} as const;\n\nconst EASE_OUT = Easing.bezier(0.32, 0.72, 0, 1);\nconst EASE_IO = Easing.bezier(0.65, 0, 0.35, 1);\nconst EXIT = Easing.bezier(0.55, 0, 1, 0.45);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\nconst CHIP = { mass: 0.5, damping: 12, stiffness: 200 };\nconst STOP = 54;\nconst GROW = [42, 68] as const;\nconst CLEAR = [40, 62] as const;\nconst TINT = [72, 88] as const;\nconst RULE = [78, 96] as const;\nconst RESOLVE = 104;\nconst LOGO_IN = 132;\n\nfunction width(text: string) {\n  let x = 0;\n  for (const ch of text) {\n    const code = ch.charCodeAt(0);\n    x += (code >= 32 && code < 127 ? ADV[code - 32] : 560) / 1000 + HERO_TRACK;\n  }\n  return x;\n}\nfunction split(q: string, term: string) {\n  if (!term) return [{ text: q, hit: false }];\n  const parts: { text: string; hit: boolean }[] = [];\n  let rest = q;\n  for (;;) {\n    const at = rest.indexOf(term);\n    if (at < 0) break;\n    if (at > 0) parts.push({ text: rest.slice(0, at), hit: false });\n    parts.push({ text: term, hit: true });\n    rest = rest.slice(at + term.length);\n  }\n  if (rest) parts.push({ text: rest, hit: false });\n  return parts;\n}\n\nexport function QueryCascade({\n  queries = QUERIES,\n  hero = \"how to make a launch video\",\n  term = \"launch video\",\n  answerBefore = \"Your\",\n  answerAfter = \"is ready.\",\n  showLogo = true,\n  theme = \"dark\",\n  ground,\n  groundParams,\n  speed = 1,\n}: QueryCascadeProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H, fps } = useVideoConfig();\n  const tall = H > W;\n  const u = tall ? W / 1000 : H / 720;\n  const c = THEMES[theme];\n  const dark = theme === \"dark\";\n  const resolvedGround: GshPresetId | undefined =\n    ground ?? (groundParams ? undefined : dark ? \"ember\" : undefined);\n  const resolvedParams = groundParams ?? (dark ? undefined : LIGHT_GROUND);\n\n  const list = useMemo(() => {\n    const at = Math.max(0, queries.length - HERO_FROM_END + 1);\n    return [...queries.slice(0, at), hero, ...queries.slice(at)];\n  }, [queries, hero]);\n  const K = list.indexOf(hero);\n\n  const widest = Math.max(...list.map(width));\n  const f = Math.min(50 * u, (W * (tall ? 0.76 : 0.84)) / widest);\n  const pitch = 76 * u;\n  const s0 = H / 2 / pitch + 0.6;\n  const scroll = (x: number) => {\n    const p = Math.min(1, Math.max(0, x / STOP));\n    return s0 + (K - s0) * (1 - (1 - p) ** 3.2);\n  };\n  const s = scroll(t);\n  const vel = Math.abs(scroll(t + 0.5) - scroll(t - 0.5)) * pitch;\n  const smear = Math.min(110 * u, vel * 0.6);\n  const taps = smear < 1.5 ? 1 : Math.min(28, Math.ceil(smear / (2 * u)) + 1);\n  const tapOffsets = Array.from({ length: taps }, (_, j) =>\n    taps === 1 ? 0 : (j / (taps - 1) - 0.5) * smear,\n  );\n  const tapOpacity = taps === 1 ? 1 : Math.min(1, 2.4 / taps);\n  const termAt = hero.indexOf(term);\n  const qLeft = termAt >= 0 ? hero.slice(0, termAt) : hero;\n  const qRight = termAt >= 0 ? hero.slice(termAt + term.length) : \"\";\n  const wTerm = width(term);\n  const totalQ = width(qLeft) + wTerm + width(qRight);\n  const aLeft = answerBefore ? `${answerBefore} ` : \"\";\n  const aRight = answerAfter ? ` ${answerAfter}` : \"\";\n  const totalA = width(aLeft) + wTerm + width(aRight);\n  const Fh = Math.min(\n    (tall ? 96 : 80) * u,\n    (W * (tall ? 0.6 : 0.58)) / Math.max(totalQ, totalA),\n  );\n\n  const grow = interpolate(t, [GROW[0], GROW[1]], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const fs = f + (Fh - f) * grow;\n  const clear = interpolate(t, [CLEAR[0], CLEAR[1]], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const tint = interpolate(t, [TINT[0], TINT[1]], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const rule = interpolate(t, [RULE[0], RULE[1]], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const move = interpolate(t, [RESOLVE + 2, RESOLVE + 24], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const leave = interpolate(t, [RESOLVE, RESOLVE + 12], [0, 1], {\n    ...CLAMP,\n    easing: EXIT,\n  });\n  const logoIn = interpolate(t, [LOGO_IN, LOGO_IN + 16], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n\n  const cx = W / 2;\n  const heroY = H / 2 + (K - s) * pitch + fs * 0.27 - grow * 18 * u;\n  const termQ = -totalQ / 2 + width(qLeft);\n  const termA = -totalA / 2 + width(aLeft);\n  const termX = termQ + (termA - termQ) * move;\n\n  const dim = theme === \"dark\" ? 0.42 : 0.5;\n  const heroBright = interpolate(t, [32, 54], [dim, 1], CLAMP);\n  const heroEdge = Math.min(\n    1,\n    Math.max(0, Math.min(heroY, H - heroY) / (H * 0.3)),\n  );\n\n  const fieldIn = spring({\n    frame: t - (STOP - 10),\n    fps,\n    config: { mass: 0.8, damping: 18, stiffness: 160 },\n  });\n  const fieldOut = interpolate(t, [RESOLVE - 4, RESOLVE + 6], [0, 1], {\n    ...CLAMP,\n    easing: EXIT,\n  });\n  const fieldVis = Math.min(1, fieldIn * 1.4) * (1 - fieldOut);\n  const fieldH = fs * 1.9;\n  const iconS = fs * 0.62;\n  const keyS = fs * 0.92;\n  const fieldW = totalQ * fs + fs * 2.0 + iconS + keyS + fs * 0.5;\n  const enter = Math.max(\n    0,\n    spring({ frame: t - (RESOLVE - 12), fps, config: CHIP }) -\n      spring({ frame: t - (RESOLVE - 5), fps, config: CHIP }),\n  );\n  const fieldLeft = cx - (totalQ * fs) / 2 - fs * 0.5 - iconS - fs * 0.8;\n\n  const textStyle = {\n    position: \"absolute\" as const,\n    whiteSpace: \"pre\" as const,\n    lineHeight: 1,\n    fontFamily: grotesk,\n    fontWeight: 400,\n  };\n\n  const word = (\n    text: string,\n    x: number,\n    anchorRight: boolean,\n    opacity: number,\n    extra?: React.CSSProperties,\n  ) =>\n    opacity > 0.001 ? (\n      <div\n        style={{\n          ...textStyle,\n          left: cx + x * fs,\n          top: heroY - fs * BASELINE,\n          fontSize: fs,\n          letterSpacing: `${HERO_TRACK}em`,\n          color: c.fg,\n          opacity,\n          transform: anchorRight ? \"translateX(-100%)\" : undefined,\n          ...extra,\n        }}\n      >\n        {text}\n      </div>\n    ) : null;\n  const arrive = (d: number) =>\n    interpolate(t, [RESOLVE + 10 + d, RESOLVE + 26 + d], [0, 1], {\n      ...CLAMP,\n      easing: EASE_OUT,\n    });\n  const before = arrive(0);\n  const after = arrive(5);\n\n  return (\n    <AbsoluteFill style={{ background: c.base, overflow: \"hidden\" }}>\n      <GshGround\n        preset={resolvedGround}\n        params={resolvedParams}\n        timeOffset={dark ? 4 : 2}\n        timeScale={0.45}\n      />\n      <AbsoluteFill style={{ background: c.scrim }} />\n      {clear < 1 ? (\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            filter: taps > 1 ? `blur(${1.2 * u}px)` : undefined,\n            WebkitMaskImage:\n              \"linear-gradient(180deg, transparent 0%, #000 30%, #000 70%, transparent 100%)\",\n            maskImage:\n              \"linear-gradient(180deg, transparent 0%, #000 30%, #000 70%, transparent 100%)\",\n          }}\n        >\n          {list.map((q, i) => {\n            if (i === K) return null;\n            const y0 = H / 2 + (i - s) * pitch;\n            const spread = (i - K) * pitch * clear * 1.4;\n            const y = y0 + spread;\n            if (y < -pitch - smear || y > H + pitch + smear) return null;\n            return tapOffsets.map((o) => (\n              <div\n                key={`${i}-${o.toFixed(2)}`}\n                style={{\n                  ...textStyle,\n                  left: 0,\n                  right: 0,\n                  top: y + o + f * 0.27 - f * BASELINE,\n                  textAlign: \"center\",\n                  fontSize: f,\n                  letterSpacing: \"-0.015em\",\n                  color: c.fg,\n                  opacity: (1 - clear) * tapOpacity,\n                }}\n              >\n                {split(q, term).map((p, j) => (\n                  <span\n                    key={`${j}-${p.text}`}\n                    style={{ opacity: p.hit ? 0.78 : dim }}\n                  >\n                    {p.text}\n                  </span>\n                ))}\n              </div>\n            ));\n          })}\n        </div>\n      ) : null}\n      {fieldVis > 0.001 ? (\n        <div\n          style={{\n            position: \"absolute\",\n            left: fieldLeft,\n            top: heroY - fs * 0.3 - fieldH / 2,\n            width: fieldW,\n            height: fieldH,\n            borderRadius: fieldH / 2,\n            background: c.field,\n            boxShadow: c.fieldDepth,\n            opacity: fieldVis,\n            transform: `scale(${0.94 + 0.06 * fieldIn - 0.02 * fieldOut})`,\n          }}\n        >\n          <svg\n            viewBox=\"0 0 24 24\"\n            width={iconS}\n            height={iconS}\n            style={{\n              position: \"absolute\",\n              left: fs * 0.8,\n              top: (fieldH - iconS) / 2,\n            }}\n            aria-hidden\n          >\n            <g\n              fill=\"none\"\n              stroke={c.icon}\n              strokeWidth={1.5}\n              strokeLinecap=\"round\"\n            >\n              <circle cx=\"10.5\" cy=\"10.5\" r=\"6.5\" />\n              <path d=\"m15.5 15.5 5 5\" />\n            </g>\n          </svg>\n          <div\n            style={{\n              position: \"absolute\",\n              right: fs * 0.55,\n              top: (fieldH - keyS) / 2,\n              width: keyS,\n              height: keyS,\n              borderRadius: keyS * 0.26,\n              display: \"flex\",\n              alignItems: \"center\",\n              justifyContent: \"center\",\n              background: interpolateColors(\n                enter,\n                [0, 1],\n                [dark ? \"rgba(255,255,255,0.04)\" : \"#F7F4F0\", c.term],\n              ),\n              boxShadow: `inset 0 0 0 1px ${c.keyRing}`,\n              transform: `scale(${1 - enter * 0.04})`,\n            }}\n          >\n            <svg\n              viewBox=\"0 0 24 24\"\n              width={keyS * 0.56}\n              height={keyS * 0.56}\n              aria-hidden\n            >\n              <path\n                d=\"M19 5.5v6a3 3 0 0 1-3 3H5.5m4-4-4 4 4 4\"\n                fill=\"none\"\n                stroke={interpolateColors(\n                  enter,\n                  [0, 1],\n                  [c.icon, dark ? \"#1A0E06\" : \"#FFFFFF\"],\n                )}\n                strokeWidth={1.5}\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n              />\n            </svg>\n          </div>\n        </div>\n      ) : null}\n\n      {tapOffsets.map((o) => (\n        <div\n          key={o.toFixed(2)}\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            opacity: heroEdge * tapOpacity,\n            transform: o ? `translateY(${o}px)` : undefined,\n          }}\n        >\n          {word(qLeft, termX, true, heroBright * (1 - leave), {\n            filter: leave > 0 ? `blur(${leave * 10 * u}px)` : undefined,\n            transform: `translateX(calc(-100% - ${leave * 30 * u}px))`,\n          })}\n          {word(qRight, termX + wTerm, false, heroBright * (1 - leave), {\n            filter: leave > 0 ? `blur(${leave * 10 * u}px)` : undefined,\n            transform: `translateX(${leave * 30 * u}px)`,\n          })}\n          {word(aLeft, termX, true, before, {\n            filter: before < 1 ? `blur(${(1 - before) * 10 * u}px)` : undefined,\n            transform: `translateX(calc(-100% + ${(1 - before) * -24 * u}px))`,\n          })}\n          {word(aRight, termX + wTerm, false, after, {\n            filter: after < 1 ? `blur(${(1 - after) * 10 * u}px)` : undefined,\n            transform: `translateX(${(1 - after) * 24 * u}px)`,\n          })}\n          <div\n            style={{\n              ...textStyle,\n              left: cx + termX * fs,\n              top: heroY - fs * BASELINE,\n              fontSize: fs,\n              letterSpacing: `${HERO_TRACK}em`,\n              color: interpolateColors(tint, [0, 1], [c.fg, c.term]),\n              opacity: Math.max(heroBright, tint),\n            }}\n          >\n            {term}\n          </div>\n          <div\n            style={{\n              position: \"absolute\",\n              left: cx + termX * fs,\n              top: heroY + fs * 0.16,\n              width: wTerm * fs,\n              height: Math.max(2, fs * 0.05),\n              borderRadius: 99,\n              background: c.term,\n              transform: `scaleX(${rule})`,\n              transformOrigin: \"0 50%\",\n            }}\n          />\n        </div>\n      ))}\n\n      {showLogo && logoIn > 0 ? (\n        <div\n          style={{\n            position: \"absolute\",\n            left: 0,\n            right: 0,\n            top: heroY + Fh * 0.9,\n            display: \"flex\",\n            justifyContent: \"center\",\n            opacity: logoIn,\n            transform: `translateY(${(1 - logoIn) * 10 * u}px)`,\n          }}\n        >\n          <RemocnLogo fontSize={26 * u} color={c.fg} />\n        </div>\n      ) : null}\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/query-cascade.tsx"
    }
  ],
  "type": "registry:component"
}
