{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ghost-complete",
  "title": "Ghost Complete",
  "description": "Grey ghost suggestions autocomplete a sentence, each accepted with a Tab keypress.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "registryDependencies": ["@remocn/gsh"],
  "files": [
    {
      "path": "registry/remocn/ghost-complete/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadGeist } from \"@remotion/google-fonts/Geist\";\nimport { loadFont as loadGeistMono } from \"@remotion/google-fonts/GeistMono\";\nimport { loadFont as loadManrope } from \"@remotion/google-fonts/Manrope\";\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: geist } = loadGeist(\"normal\", {\n  weights: [\"300\", \"400\"],\n  subsets: [\"latin\"],\n});\nconst { fontFamily: mono } = loadGeistMono(\"normal\", {\n  weights: [\"400\"],\n  subsets: [\"latin\"],\n});\nconst manrope = loadManrope(\"normal\", {\n  weights: [\"600\"],\n  subsets: [\"latin\"],\n});\n\nexport interface GhostCompleteProps {\n  title?: string;\n  typed?: string;\n  suggestions?: string[];\n  keepLastFill?: boolean;\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\";\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\nconst ADV = [\n  243, 228, 361, 515, 643, 810, 649, 186, 290, 290, 427, 562, 213, 418, 213,\n  494, 673, 406, 630, 625, 629, 641, 604, 531, 624, 606, 302, 302, 546, 544,\n  546, 570, 925, 689, 688, 713, 701, 609, 595, 713, 716, 280, 607, 656, 583,\n  890, 745, 751, 657, 745, 680, 654, 568, 694, 688, 968, 633, 594, 561, 361,\n  470, 361, 438, 558, 258, 565, 608, 563, 608, 576, 412, 607, 591, 256, 284,\n  609, 282, 885, 591, 588, 608, 608, 394, 537, 410, 586, 560, 829, 607, 553,\n  552, 395, 274, 395, 523,\n];\nconst TRACK = -0.035;\nconst LEADING = 1.08;\n\nconst LIGHT_GROUND: Partial<GradientParams> = {\n  kind: \"mist\",\n  colors: [\"#F2955A\", \"#F7CBA8\", \"#F3E6D8\", \"#6B2A12\"],\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: 9,\n};\n\nconst THEMES = {\n  dark: {\n    base: \"#050303\",\n    scrim:\n      \"linear-gradient(180deg, rgba(5,3,3,0.55) 0%, rgba(5,3,3,0.2) 70%, rgba(5,3,3,0) 100%)\",\n    card: \"rgba(17,14,13,0.9)\",\n    depth:\n      \"inset 0 1px 0 rgba(255,255,255,0.06), 0 0 0 1px rgba(255,255,255,0.08), 0 30px 60px -20px rgba(0,0,0,0.75)\",\n    divider: \"rgba(255,255,255,0.07)\",\n    fg: \"#F4F1EC\",\n    ghost: \"#5C5650\",\n    fill: \"#FFB27A\",\n    dim: \"#8F8780\",\n    key: \"rgba(255,255,255,0.14)\",\n    keyBg: \"rgba(255,255,255,0.04)\",\n    logo: \"#F4F1EC\",\n  },\n  light: {\n    base: \"#F2EEE8\",\n    scrim:\n      \"linear-gradient(180deg, rgba(246,243,238,0.5) 0%, rgba(246,243,238,0.1) 100%)\",\n    card: \"rgba(255,255,255,0.94)\",\n    depth:\n      \"0 0 0 1px rgba(40,24,12,0.06), 0 1px 3px rgba(40,24,12,0.05), 0 30px 60px -24px rgba(90,40,10,0.25)\",\n    divider: \"rgba(40,24,12,0.07)\",\n    fg: \"#17120F\",\n    ghost: \"#C2BAB1\",\n    fill: \"#C2410C\",\n    dim: \"#857B73\",\n    key: \"rgba(40,24,12,0.16)\",\n    keyBg: \"#F7F4F0\",\n    logo: \"#17120F\",\n  },\n} as const;\n\nconst ENTER = Easing.bezier(0.32, 0.72, 0, 1);\nconst MOVE = 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;\n\nconst TYPE_START = 6;\nconst PER_CHAR = 2;\nconst GHOST_LEAD = 4;\nconst HOLD_GHOST = 16;\nconst AFTER_FILL = 8;\nconst SETTLE = 12;\n\nfunction advance(ch: string) {\n  const code = ch.charCodeAt(0);\n  return (code >= 32 && code < 127 ? ADV[code - 32] : 600) / 1000;\n}\n\ninterface Glyph {\n  ch: string;\n  seg: number;\n  i: number;\n}\n\ninterface Segment {\n  start: number;\n  length: number;\n  breaks: number;\n  ghostIn: number;\n  accept: number;\n  fillDur: number;\n}\n\nexport function GhostComplete({\n  title = \"Launch notes\",\n  typed = \"Ship the\",\n  suggestions = [\"launch video\", \"\\nbefore lunch.\"],\n  keepLastFill = true,\n  showLogo = true,\n  theme = \"dark\",\n  ground,\n  groundParams,\n  speed = 1,\n}: GhostCompleteProps) {\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 ? \"hearth\" : undefined);\n  const resolvedParams = groundParams ?? (dark ? undefined : LIGHT_GROUND);\n\n  const plan = useMemo(() => {\n    const glyphs: Glyph[] = Array.from(typed).map((ch, i) => ({\n      ch,\n      seg: -1,\n      i,\n    }));\n    const segs: Segment[] = [];\n    let clock = TYPE_START + typed.length * PER_CHAR + GHOST_LEAD;\n    suggestions.forEach((raw, k) => {\n      const text =\n        raw.startsWith(\"\\n\") || raw.startsWith(\" \") ? raw : ` ${raw}`;\n      const chars = Array.from(text);\n      const start = glyphs.length;\n      chars.forEach((ch, i) => {\n        glyphs.push({ ch, seg: k, i });\n      });\n      const fillDur = Math.max(7, Math.min(12, chars.length * 0.6));\n      const ghostIn = clock;\n      const accept = ghostIn + HOLD_GHOST + (k === 0 ? 6 : 0);\n      segs.push({\n        start,\n        length: chars.length,\n        breaks: chars.filter((ch) => ch === \"\\n\").length,\n        ghostIn,\n        accept,\n        fillDur,\n      });\n      clock = accept + fillDur + AFTER_FILL;\n    });\n    const lines: number[] = [0];\n    for (const g of glyphs) {\n      if (g.ch === \"\\n\") lines.push(0);\n      else lines[lines.length - 1] += advance(g.ch) + TRACK;\n    }\n    return {\n      glyphs,\n      segs,\n      widest: Math.max(...lines) - TRACK,\n      lineCount: lines.length,\n      end: clock,\n    };\n  }, [typed, suggestions]);\n  const { glyphs, segs } = plan;\n  const last = segs[segs.length - 1];\n\n  const F = Math.min(\n    (tall ? 104 : 112) * u,\n    (W * (tall ? 0.72 : 0.62)) / plan.widest,\n    (H * 0.4) / (plan.lineCount * LEADING),\n  );\n  const lineH = F * LEADING;\n  const blockW = plan.widest * F;\n\n  const padX = 44 * u;\n  const headH = 66 * u;\n  const padTop = headH + 14 * u;\n  const footH = tall ? 16 * u : 72 * u;\n  const cardW = blockW + padX * 2 + F * 0.2;\n\n  const visLines =\n    1 +\n    segs.reduce(\n      (n, s) =>\n        n +\n        s.breaks *\n          spring({\n            frame: t - s.ghostIn + 2,\n            fps,\n            config: { damping: 200 },\n            durationInFrames: 14,\n          }),\n      0,\n    );\n  const cardH = padTop + visLines * lineH + 28 * u + footH;\n  const finalH = padTop + plan.lineCount * lineH + 28 * u + footH;\n  const cardLeft = (W - cardW) / 2;\n  const cardTop = (tall ? 0.38 : 0.47) * H - finalH / 2;\n  const bigKeyY = cardTop + finalH + 0.13 * H;\n\n  const cardIn = spring({\n    frame: t,\n    fps,\n    config: { mass: 0.8, damping: 18, stiffness: 160 },\n  });\n\n  const typedCount = Math.max(\n    0,\n    Math.min(typed.length, Math.floor((t - TYPE_START) / PER_CHAR) + 1),\n  );\n  let caret = typedCount;\n  let busy = t < TYPE_START + typed.length * PER_CHAR + 2;\n  const fronts = segs.map((s) => {\n    const k = interpolate(t, [s.accept, s.accept + s.fillDur], [0, 1], {\n      ...CLAMP,\n      easing: MOVE,\n    });\n    const front = k * s.length;\n    if (t >= s.accept) caret = s.start + Math.min(s.length, Math.ceil(front));\n    if (t >= s.accept - 2 && t <= s.accept + s.fillDur + 3) busy = true;\n    return front;\n  });\n  const phase = (t % 30) / 30;\n  const blink = busy\n    ? 1\n    : interpolate(phase, [0, 0.45, 0.55, 0.95, 1], [1, 1, 0, 0, 1], CLAMP);\n\n  const styleOf = (g: Glyph): React.CSSProperties => {\n    if (g.seg < 0) return { color: c.fg };\n    const s = segs[g.seg];\n    const appear = interpolate(\n      t,\n      [s.ghostIn + g.i * 0.35, s.ghostIn + g.i * 0.35 + 6],\n      [0, 1],\n      { ...CLAMP, easing: ENTER },\n    );\n    const filled = Math.min(1, Math.max(0, fronts[g.seg] - g.i));\n    const settleAt = s.accept + s.fillDur + AFTER_FILL;\n    const settle =\n      keepLastFill && g.seg === segs.length - 1\n        ? 0\n        : interpolate(t, [settleAt, settleAt + SETTLE], [0, 1], {\n            ...CLAMP,\n            easing: MOVE,\n          });\n    if (filled <= 0) return { color: c.ghost, opacity: appear };\n    if (settle > 0) {\n      return { color: interpolateColors(settle, [0, 1], [c.fill, c.fg]) };\n    }\n    return { color: interpolateColors(filled, [0, 1], [c.ghost, c.fill]) };\n  };\n\n  const hintOut = interpolate(\n    t,\n    [last.accept + last.fillDur + 4, last.accept + last.fillDur + 12],\n    [0, 1],\n    { ...CLAMP, easing: EXIT },\n  );\n  const hintArmed = interpolate(\n    t,\n    [segs[0].ghostIn + 2, segs[0].ghostIn + 12],\n    [0, 1],\n    { ...CLAMP, easing: ENTER },\n  );\n  const press = segs.reduce((p, s) => {\n    const down = spring({\n      frame: t - s.accept + 3,\n      fps,\n      config: { mass: 0.5, damping: 12, stiffness: 200 },\n    });\n    const up = spring({\n      frame: t - s.accept - 2,\n      fps,\n      config: { mass: 0.5, damping: 12, stiffness: 200 },\n    });\n    return Math.max(p, Math.max(0, down - up));\n  }, 0);\n  const logoIn = interpolate(t, [plan.end - 2, plan.end + 12], [0, 1], {\n    ...CLAMP,\n    easing: ENTER,\n  });\n  const push = interpolate(t, [0, plan.end + 40], [1, 1.03], {\n    ...CLAMP,\n    easing: MOVE,\n  });\n\n  const caretEl = (\n    <span\n      key=\"caret\"\n      style={{\n        display: \"inline-block\",\n        position: \"relative\",\n        width: 0,\n        height: \"0.86em\",\n        verticalAlign: \"-0.1em\",\n      }}\n    >\n      <span\n        style={{\n          position: \"absolute\",\n          left: \"0.035em\",\n          top: 0,\n          bottom: 0,\n          width: Math.max(2, 0.04 * F),\n          borderRadius: 2,\n          background: c.fill,\n          opacity: blink,\n        }}\n      />\n    </span>\n  );\n\n  const nodes: React.ReactNode[] = [];\n  glyphs.forEach((g, idx) => {\n    if (idx === caret) nodes.push(caretEl);\n    const visible =\n      g.seg < 0 ? idx < typedCount : t >= segs[g.seg].ghostIn - 0.5;\n    if (!visible) return;\n    if (g.ch === \"\\n\") {\n      nodes.push(<br key={idx} />);\n      return;\n    }\n    nodes.push(\n      <span key={idx} style={styleOf(g)}>\n        {g.ch}\n      </span>,\n    );\n  });\n  if (caret >= glyphs.length) nodes.push(caretEl);\n\n  const label = (size: number): React.CSSProperties => ({\n    fontFamily: geist,\n    fontWeight: 400,\n    fontSize: size,\n    letterSpacing: \"-0.01em\",\n    color: c.dim,\n  });\n\n  const keyCap = (k: number, raised: boolean) => (\n    <div\n      style={{\n        height: 34 * k,\n        padding: `0 ${12 * k}px`,\n        display: \"flex\",\n        alignItems: \"center\",\n        borderRadius: 8 * k,\n        background: interpolateColors(\n          press,\n          [0, 1],\n          [raised ? c.card : c.keyBg, c.fill],\n        ),\n        boxShadow: `inset 0 0 0 1px ${c.key}, 0 ${(1 - press) * 2 * k}px 0 ${c.key}${raised ? `, ${c.depth}` : \"\"}`,\n        transform: `translateY(${press * 2 * k}px) scale(${1 - press * 0.04})`,\n        color: interpolateColors(\n          press,\n          [0, 1],\n          [c.fg, dark ? \"#1A0E06\" : \"#FFFFFF\"],\n        ),\n        fontFamily: mono,\n        fontWeight: 400,\n        fontSize: 17 * k,\n        letterSpacing: \"0.02em\",\n      }}\n    >\n      Tab\n    </div>\n  );\n\n  const keyIn = spring({\n    frame: t - segs[0].ghostIn + 4,\n    fps,\n    config: { mass: 0.8, damping: 18, stiffness: 160 },\n  });\n\n  return (\n    <AbsoluteFill style={{ background: c.base, overflow: \"hidden\" }}>\n      <GshGround\n        preset={resolvedGround}\n        params={resolvedParams}\n        timeOffset={dark ? 6 : 2}\n        timeScale={0.45}\n      />\n      <AbsoluteFill style={{ background: c.scrim }} />\n      <AbsoluteFill style={{ transform: `scale(${push})` }}>\n        <div\n          style={{\n            position: \"absolute\",\n            left: cardLeft,\n            top: cardTop,\n            width: cardW,\n            height: cardH,\n            borderRadius: 28 * u,\n            background: c.card,\n            boxShadow: c.depth,\n            overflow: \"hidden\",\n            opacity: interpolate(cardIn, [0, 0.4], [0, 1], CLAMP),\n            transform: `translateY(${(1 - cardIn) * 40 * u}px) scale(${0.96 + 0.04 * cardIn})`,\n          }}\n        >\n          <div\n            style={{\n              position: \"absolute\",\n              left: padX,\n              top: 0,\n              height: headH,\n              display: \"flex\",\n              alignItems: \"flex-end\",\n              gap: 10 * u,\n            }}\n          >\n            <svg\n              viewBox=\"0 0 24 24\"\n              width={(tall ? 28 : 22) * u}\n              height={(tall ? 28 : 22) * u}\n              style={{ marginBottom: 1 * u }}\n              aria-hidden\n            >\n              <g\n                fill=\"none\"\n                stroke={c.dim}\n                strokeWidth={1.5}\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n              >\n                <path d=\"M6.5 3.5h7l4 4v13h-11z\" />\n                <path d=\"M13.5 3.5v4h4M9.5 12.5h5M9.5 16h5\" />\n              </g>\n            </svg>\n            <div style={label((tall ? 26 : 19) * u)}>{title}</div>\n          </div>\n          <div\n            style={{\n              position: \"absolute\",\n              left: padX,\n              top: padTop,\n              width: blockW + F,\n              fontFamily: geist,\n              fontWeight: 400,\n              fontSize: F,\n              lineHeight: LEADING,\n              letterSpacing: `${TRACK}em`,\n              whiteSpace: \"pre\",\n              fontKerning: \"normal\",\n            }}\n          >\n            {nodes}\n          </div>\n          {tall ? null : (\n            <div\n              style={{\n                position: \"absolute\",\n                left: 0,\n                right: 0,\n                bottom: 0,\n                height: footH,\n                borderTop: `1px solid ${c.divider}`,\n                display: \"flex\",\n                alignItems: \"center\",\n                justifyContent: \"space-between\",\n                padding: `0 ${padX - 10 * u}px 0 ${padX}px`,\n              }}\n            >\n              <div\n                style={{\n                  display: \"flex\",\n                  alignItems: \"center\",\n                  gap: 12 * u,\n                  opacity: (0.35 + 0.65 * hintArmed) * (1 - hintOut),\n                }}\n              >\n                {keyCap(u, false)}\n                <div style={label(19 * u)}>to accept</div>\n              </div>\n              {showLogo ? (\n                <div\n                  style={{\n                    opacity: logoIn,\n                    transform: `translateY(${(1 - logoIn) * 8 * u}px)`,\n                  }}\n                >\n                  <RemocnLogo fontSize={24 * u} color={c.logo} />\n                </div>\n              ) : null}\n            </div>\n          )}\n        </div>\n        {tall ? (\n          <div\n            style={{\n              position: \"absolute\",\n              left: 0,\n              right: 0,\n              top: bigKeyY,\n              display: \"flex\",\n              flexDirection: \"column\",\n              alignItems: \"center\",\n              gap: 30 * u,\n              opacity:\n                Math.min(1, keyIn * 1.4) *\n                (0.35 + 0.65 * hintArmed) *\n                (1 - 0.5 * hintOut),\n              transform: `translateY(${(1 - keyIn) * 40 * u}px)`,\n            }}\n          >\n            {keyCap(3.4 * u, true)}\n            <div style={label(34 * u)}>to accept</div>\n          </div>\n        ) : null}\n        {tall && showLogo ? (\n          <div\n            style={{\n              position: \"absolute\",\n              left: 0,\n              right: 0,\n              top: bigKeyY + 290 * u,\n              display: \"flex\",\n              justifyContent: \"center\",\n              opacity: logoIn,\n              transform: `translateY(${(1 - logoIn) * 12 * u}px)`,\n            }}\n          >\n            <RemocnLogo fontSize={40 * u} color={c.logo} />\n          </div>\n        ) : null}\n      </AbsoluteFill>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/ghost-complete.tsx"
    }
  ],
  "type": "registry:component"
}
