{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "click-type",
  "title": "Click Type",
  "description": "Click to type: a prompt composer (Prompt Send's glass panel, writing surface and round send button) whose placeholder writes itself while a cursor flies in from the edge and clicks the field, then a hard cut into the field where the real prompt types at the caret and send arms. Two shots of one composer under a constant camera, anchored on the first letter, so the type never scales while it moves. Built on cursor-track.",
  "dependencies": [
    "remotion",
    "@remotion/google-fonts"
  ],
  "registryDependencies": [
    "https://snapcn.dev/r/snap-cn-ui.json",
    "https://snapcn.dev/r/input.json",
    "https://snapcn.dev/r/caret.json",
    "https://snapcn.dev/r/cursor-track.json"
  ],
  "files": [
    {
      "path": "registry/snap-cn/click-type/index.tsx",
      "content": "// Click Type · snapcn — https://snapcn.dev/docs/cursor/click-type\n\"use client\";\n\nimport { loadFont as loadSans } from \"@remotion/google-fonts/Inter\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\nimport { Caret } from \"@/components/snap-cn/caret\";\nimport { CursorTrack } from \"@/components/snap-cn/cursor-track\";\nimport { inputStyleContext } from \"@/components/snap-cn/input\";\nimport {\n  mixOklch,\n  type SnapCnTheme,\n  useSnapCnTheme,\n  withAlpha,\n} from \"@/lib/snap-cn-ui\";\n\nconst { fontFamily: SANS } = loadSans(\"normal\", {\n  weights: [\"400\", \"500\"],\n  subsets: [\"latin\"],\n});\n\n/**\n * Click to type: a prompt composer whose placeholder writes itself while a\n * cursor flies in and clicks the field — then a hard cut into the field, where\n * the real prompt types at the caret and the send button arms.\n *\n * The composer is Prompt Send's: a glass panel over a soft accent wash, a\n * writing surface inside it, a round ink send button. Same paint, so the two\n * cut together.\n *\n * Two shots of one composer. The close-up is the same scene under a constant\n * camera scale, anchored on the text's first letter, so nothing scales while\n * it moves and the type has no pixel grid to climb.\n */\nexport interface ClickTypeProps {\n  /** Muted text that writes itself in the wide shot. */\n  placeholder?: string;\n  /** What gets typed after the click, in the close-up. */\n  prompt?: string;\n  /** Small label at the field's bottom-left. */\n  hint?: string;\n  /** The wash behind the composer. Defaults to the theme's primary. */\n  accentColor?: string;\n  theme?: Partial<SnapCnTheme>;\n  mode?: \"light\" | \"dark\";\n  /** Time multiplier. */\n  speed?: number;\n  className?: string;\n}\n\n/** Cursor lands and clicks. */\nexport const CLICK_AT = 30;\n/** The cut into the field. */\nexport const CUT_AT = 34;\nconst TYPE_AT = CUT_AT + 4;\nconst HOLD = 36;\n\n/** At 720p. */\nconst PANEL_W = 920;\nconst PANEL_H = 260;\n/** The glass rim between panel and writing surface. */\nconst PAD = 7;\nconst RADIUS = 32;\nconst PAD_X = 34;\nconst PAD_Y = 34;\nconst TEXT = 30;\nconst SEND = 46;\nconst ZOOM = 1.9;\n/** Where the first letter lands in the close-up, as a fraction of the frame. */\nconst FOCUS = { x: 0.2, y: 0.4 };\n\nconst EASE = Easing.bezier(0.2, 0.6, 0.35, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\n/** Frame each character of `prompt` lands on, relative to TYPE_AT. A space costs a beat. */\nfunction keystrokes(prompt: string) {\n  const at: number[] = [];\n  let f = 0;\n  for (const ch of prompt) {\n    at.push(f);\n    f += ch === \" \" ? 2 : 1;\n  }\n  return { at, total: f };\n}\n\nexport const DEFAULT_PROMPT = \"Build me a marketing site\";\n\n/** Natural length for a given prompt. */\nexport function clickTypeFrames(prompt: string = DEFAULT_PROMPT): number {\n  return TYPE_AT + keystrokes(prompt).total + HOLD;\n}\n\nconst PLUS = \"M12 7v10M7 12h10\";\nconst MIC =\n  \"M12 4.5a2.5 2.5 0 0 0-2.5 2.5v4.5a2.5 2.5 0 0 0 5 0V7A2.5 2.5 0 0 0 12 4.5zM7 11a5 5 0 0 0 10 0M12 16v3\";\nconst SPARK = \"M12 4v4M12 16v4M4 12h4M16 12h4M6.5 6.5l2.5 2.5M15 15l2.5 2.5\";\nconst ARROW = \"M12 19V5M5.5 11.5 12 5l6.5 6.5\";\n\nexport function ClickType({\n  placeholder = \"Describe what you want to build...\",\n  prompt = DEFAULT_PROMPT,\n  hint = \"New suggestion\",\n  accentColor,\n  theme,\n  mode,\n  speed = 1,\n  className,\n}: ClickTypeProps) {\n  const frame = useCurrentFrame() * speed;\n  const { width, height } = useVideoConfig();\n  const t = useSnapCnTheme(theme, mode);\n  const ui = inputStyleContext(t);\n  const s = height / 720;\n  const accent = accentColor ?? t.primary;\n\n  // Prompt Send's paint, token for token.\n  const glassy = withAlpha(t.card, 0.52);\n  const surface = withAlpha(t.card, 0.94);\n  const hairline = mixOklch(ui.idleBorder, t.background, 0.1);\n  const sendIdle = mixOklch(t.foreground, t.card, 0.55);\n\n  const close = frame >= CUT_AT;\n  const panelX = (width - PANEL_W * s) / 2;\n  const panelY = (height - PANEL_H * s) / 2;\n  const fieldX = panelX + PAD * s;\n  const fieldY = panelY + PAD * s;\n  const fieldW = (PANEL_W - PAD * 2) * s;\n  const fieldH = (PANEL_H - PAD * 2) * s;\n  const textX = fieldX + PAD_X * s;\n  const textY = fieldY + PAD_Y * s;\n\n  // Placeholder writes itself at ~1.5 chars a frame from frame 4.\n  const shown = Math.max(0, Math.floor((frame - 4) * 1.5));\n  const typed = keystrokes(prompt).at.filter(\n    (a) => frame >= TYPE_AT + a,\n  ).length;\n  const armed = close && typed > 0;\n  const enter = interpolate(frame, [0, 12], [0, 1], { ...CLAMP, easing: EASE });\n\n  const path = [\n    { at: 6, x: -0.04, y: 0.62, duration: 4 },\n    {\n      at: 10,\n      x: (textX + 8 * s) / width,\n      y: (textY + TEXT * 0.9 * s) / height,\n      duration: CLICK_AT - 10,\n      click: true,\n    },\n  ];\n\n  // The close-up: one constant camera, the first letter pinned to FOCUS.\n  const camera = close\n    ? `translate(${FOCUS.x * width - textX * ZOOM}px, ${FOCUS.y * height - textY * ZOOM}px) scale(${ZOOM})`\n    : \"none\";\n\n  const glyph = (d: string, color: string, size: number, stroke = 1.8) => (\n    <svg\n      viewBox=\"0 0 24 24\"\n      width={size}\n      height={size}\n      style={{ display: \"block\" }}\n    >\n      <title>Icon</title>\n      <path\n        d={d}\n        fill=\"none\"\n        stroke={color}\n        strokeWidth={stroke}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n  const iconButton = (d: string) => (\n    <div\n      style={{\n        width: SEND * s,\n        height: SEND * s,\n        boxSizing: \"border-box\",\n        borderRadius: 999,\n        border: `1px solid ${hairline}`,\n        background: t.card,\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n      }}\n    >\n      {glyph(d, ui.mutedForeground, 22 * s)}\n    </div>\n  );\n\n  const scene = (\n    <AbsoluteFill\n      className={className}\n      style={{\n        background: t.background,\n        fontFamily: SANS,\n        textRendering: \"geometricPrecision\",\n        overflow: \"hidden\",\n      }}\n    >\n      <AbsoluteFill style={{ transformOrigin: \"0 0\", transform: camera }}>\n        {/* Prompt Send's wash: a cool field falling from the top right, a\n            bright patch left of centre, a pale corner under it — alpha over\n            the page, never a hue mix through green. Oversized so the close-up\n            never finds its edge. */}\n        <div\n          style={{\n            position: \"absolute\",\n            left: -0.2 * width,\n            top: -0.2 * height,\n            width: 1.4 * width,\n            height: 1.4 * height,\n            background: t.background,\n            backgroundImage: [\n              `radial-gradient(${0.96 * width}px ${0.8 * height}px at ${0.26 * width}px ${1.18 * height}px, ${withAlpha(t.background, 0.9)} 0%, transparent 62%)`,\n              `radial-gradient(${0.74 * width}px ${0.62 * height}px at ${0.3 * width}px ${0.66 * height}px, ${withAlpha(t.card, 0.42)} 0%, transparent 66%)`,\n              `linear-gradient(215deg, ${withAlpha(accent, 0.5)} 0%, ${withAlpha(accent, 0.28)} 52%, ${withAlpha(accent, 0.14)} 100%)`,\n            ].join(\",\"),\n          }}\n        />\n        <AbsoluteFill\n          style={{\n            opacity: close ? 1 : enter,\n            transform: close\n              ? undefined\n              : `translateY(${(1 - enter) * 14 * s}px)`,\n          }}\n        >\n          {/* The glass panel. Hairline as an inset shadow, not a border. */}\n          <div\n            style={{\n              position: \"absolute\",\n              left: panelX,\n              top: panelY,\n              width: PANEL_W * s,\n              height: PANEL_H * s,\n              borderRadius: RADIUS * s,\n              background: glassy,\n              boxShadow: `inset 0 0 0 1px ${withAlpha(t.card, 0.6)}, 0 ${12 * s}px ${32 * s}px ${withAlpha(t.foreground, 0.05)}`,\n            }}\n          />\n          {/* The writing surface. */}\n          <div\n            style={{\n              position: \"absolute\",\n              left: fieldX,\n              top: fieldY,\n              width: fieldW,\n              height: fieldH,\n              borderRadius: (RADIUS - 2) * s,\n              background: surface,\n              boxShadow: `0 ${6 * s}px ${18 * s}px ${withAlpha(t.foreground, 0.05)}`,\n            }}\n          />\n          <div\n            style={{\n              position: \"absolute\",\n              left: textX,\n              top: textY,\n              right: width - (fieldX + fieldW) + PAD_X * s,\n              fontSize: TEXT * s,\n              lineHeight: 1.2,\n              fontWeight: 400,\n              letterSpacing: \"-0.01em\",\n              whiteSpace: \"nowrap\",\n              display: \"flex\",\n              alignItems: \"center\",\n            }}\n          >\n            {close ? (\n              <>\n                <span style={{ color: t.foreground }}>\n                  {prompt.slice(0, typed)}\n                </span>\n                <Caret\n                  color={t.foreground}\n                  width={Math.max(1.5, 2.4 * s)}\n                  height={TEXT * 1.15 * s}\n                  // Solid while it types, blinking once it waits.\n                  blink={typed === 0 || typed === prompt.length}\n                  blinkPerSecond={1.6}\n                  marginLeft={2 * s}\n                />\n              </>\n            ) : (\n              <span style={{ color: ui.mutedForeground }}>\n                {placeholder.slice(0, shown)}\n              </span>\n            )}\n          </div>\n          <div\n            style={{\n              position: \"absolute\",\n              left: textX,\n              right: width - (fieldX + fieldW) + 18 * s,\n              top: fieldY + fieldH - (SEND + 18) * s,\n              height: SEND * s,\n              display: \"flex\",\n              alignItems: \"center\",\n              gap: 10 * s,\n              fontSize: 19 * s,\n              fontWeight: 500,\n              color: ui.mutedForeground,\n            }}\n          >\n            {glyph(SPARK, ui.mutedForeground, 18 * s, 2)}\n            <span style={{ flex: 1 }}>{hint}</span>\n            {iconButton(PLUS)}\n            {iconButton(MIC)}\n            <div\n              style={{\n                width: SEND * s,\n                height: SEND * s,\n                borderRadius: 999,\n                background: armed ? t.foreground : sendIdle,\n                display: \"flex\",\n                alignItems: \"center\",\n                justifyContent: \"center\",\n              }}\n            >\n              {glyph(ARROW, t.card, SEND * 0.52 * s, 2.2)}\n            </div>\n          </div>\n        </AbsoluteFill>\n      </AbsoluteFill>\n    </AbsoluteFill>\n  );\n\n  // The cursor belongs to the wide shot; the cut leaves it behind.\n  return close ? (\n    scene\n  ) : (\n    <CursorTrack path={path} theme={theme} mode={mode} speed={speed}>\n      {scene}\n    </CursorTrack>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/snap-cn/click-type.tsx"
    }
  ],
  "docs": "Docs and props: https://snapcn.dev/docs/cursor/click-type?ref=cli — placeholder writes itself in the wide shot; prompt is what types after the click, in the close-up. Pass mode=\"dark\" if you are rendering onto a dark frame — these default to shadcn's light palette. — 82 Pro components too — charts, launch scenes, device shots: https://snapcn.dev/docs/pricing?ref=cli-docs",
  "type": "registry:component"
}
