{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "button-press",
  "title": "Button Press",
  "description": "Cursor clicks a button: a cursor walks onto a split action button (label, divider, chevron), it darkens on hover, gives on press, spins while pending and lands on a drawn tick. The deploy, publish or send beat of every launch video. Built on cursor-track.",
  "dependencies": [
    "remotion",
    "@remotion/google-fonts"
  ],
  "registryDependencies": [
    "https://snapcn.dev/r/snap-cn-ui.json",
    "https://snapcn.dev/r/cursor-track.json"
  ],
  "files": [
    {
      "path": "registry/snap-cn/button-press/index.tsx",
      "content": "// Button Press · snapcn — https://snapcn.dev/docs/cursor/button-press\n\"use client\";\n\nimport { loadFont as loadSans } from \"@remotion/google-fonts/Inter\";\nimport type { ReactNode } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\nimport { CursorTrack } from \"@/components/snap-cn/cursor-track\";\nimport {\n  mixOklch,\n  type SnapCnTheme,\n  useSnapCnTheme,\n  withAlpha,\n} from \"@/lib/snap-cn-ui\";\n\nconst { fontFamily: SANS } = loadSans(\"normal\", {\n  weights: [\"500\"],\n  subsets: [\"latin\"],\n});\n\n/**\n * The click every launch video has: a cursor walks onto a split action button\n * (label, divider, chevron), it darkens on hover, gives on press, spins while\n * it works and lands on a tick. Three labels, one fixed width — the button\n * never reflows, so the cursor resting on it never has to chase it.\n */\nexport interface ButtonPressProps {\n  /** Idle label. */\n  label?: string;\n  /** While it works. */\n  pending?: string;\n  /** When it is done. */\n  done?: string;\n  /** Button width at 720p. Size it for the longest label. */\n  width?: number;\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 = 36;\n/** Frames spent pending before the tick. */\nexport const PENDING_FRAMES = 40;\nexport const BUTTON_PRESS_FRAMES = 126;\n\nconst H = 76;\nconst EASE = Easing.bezier(0.2, 0.6, 0.35, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\n/** A label sliding in from below and out above, 8 frames each way. No `to`: it stays. */\nfunction phase(frame: number, from: number, to?: number) {\n  const inP = interpolate(frame, [from, from + 8], [0, 1], {\n    ...CLAMP,\n    easing: EASE,\n  });\n  const outP =\n    to === undefined\n      ? 0\n      : interpolate(frame, [to, to + 8], [0, 1], { ...CLAMP, easing: EASE });\n  return { opacity: inP * (1 - outP), y: (1 - inP) * 14 - outP * 14 };\n}\n\n/** The chevron segment's width, as a multiple of the button height. */\nconst CHEVRON_W = 0.86;\n\n/**\n * A primary split button: the action, a hairline divider, a chevron. Takes its\n * paint from the theme; the label area is whatever you pass as children. Only\n * the surface presses (`press`, 1 at rest) — the label never scales, so it has\n * no glyph snapping to fight.\n */\nexport function SplitButton({\n  t,\n  width,\n  height,\n  background,\n  press = 1,\n  children,\n}: {\n  t: SnapCnTheme;\n  /** px */\n  width: number;\n  /** px */\n  height: number;\n  /** Fill; defaults to the theme's primary. */\n  background?: string;\n  press?: number;\n  children: ReactNode;\n}) {\n  const chevron = height * CHEVRON_W;\n  return (\n    <div\n      style={{\n        position: \"relative\",\n        width,\n        height,\n        color: t.primaryForeground,\n        whiteSpace: \"nowrap\",\n      }}\n    >\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          // shadcn's button radius, kept in proportion at this height.\n          borderRadius: t.radius * (height / 36),\n          backgroundColor: background ?? t.primary,\n          scale: press,\n        }}\n      />\n      <div\n        style={{\n          position: \"absolute\",\n          left: 0,\n          top: 0,\n          bottom: 0,\n          right: chevron,\n          overflow: \"hidden\",\n        }}\n      >\n        {children}\n      </div>\n      <div\n        style={{\n          position: \"absolute\",\n          right: chevron,\n          top: height * 0.24,\n          bottom: height * 0.24,\n          width: Math.max(1, height / 76),\n          backgroundColor: withAlpha(t.primaryForeground, 0.32),\n        }}\n      />\n      <svg\n        viewBox=\"0 0 24 24\"\n        width={height * 0.34}\n        height={height * 0.34}\n        style={{\n          position: \"absolute\",\n          right: (chevron - height * 0.34) / 2,\n          top: height * 0.33,\n        }}\n      >\n        <title>More</title>\n        <path\n          d=\"M6 9.5 L12 15.5 L18 9.5\"\n          fill=\"none\"\n          stroke={t.primaryForeground}\n          strokeWidth={2.6}\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n      </svg>\n    </div>\n  );\n}\n\nexport function ButtonPress({\n  label = \"Deploy\",\n  pending = \"Deploying…\",\n  done = \"Deployed\",\n  width: w = 320,\n  theme,\n  mode,\n  speed = 1,\n  className,\n}: ButtonPressProps) {\n  const frame = useCurrentFrame() * speed;\n  const { height } = useVideoConfig();\n  const t = useSnapCnTheme(theme, mode);\n  const s = height / 720;\n\n  const doneAt = CLICK_AT + PENDING_FRAMES;\n  const path = [\n    { at: 8, x: 0.86, y: 1.08, duration: 6 },\n    { at: 14, x: 0.47, y: 0.53, duration: CLICK_AT - 14, click: true },\n    { at: doneAt + 14, x: 0.62, y: 0.7, duration: 18 },\n  ];\n\n  const enter = interpolate(frame, [0, 14], [0, 1], { ...CLAMP, easing: EASE });\n  const hover = interpolate(frame, [CLICK_AT - 6, CLICK_AT], [0, 1], CLAMP);\n  // Press returns exactly to 1, and only on the surface: the label never\n  // scales, so there is no glyph snapping to fight.\n  const pressP = interpolate(frame, [CLICK_AT, CLICK_AT + 10], [0, 1], CLAMP);\n  const press = 1 - Math.sin(pressP * Math.PI) * 0.035;\n\n  const idle = phase(frame, -8, CLICK_AT + 2);\n  const busy = phase(frame, CLICK_AT + 2, doneAt);\n  const ok = phase(frame, doneAt);\n  const tick = interpolate(frame, [doneAt + 4, doneAt + 14], [0, 1], {\n    ...CLAMP,\n    easing: EASE,\n  });\n\n  const bg = mixOklch(t.primary, t.foreground, hover * 0.14);\n  const icon = 26 * s;\n  const row = (y: number, opacity: number, children: ReactNode) => (\n    <div\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        gap: 12 * s,\n        opacity,\n        transform: `translateY(${y * s}px)`,\n      }}\n    >\n      {children}\n    </div>\n  );\n\n  return (\n    <CursorTrack path={path} theme={theme} mode={mode} speed={speed}>\n      <AbsoluteFill\n        className={className}\n        style={{\n          backgroundColor: t.background,\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          fontFamily: SANS,\n          fontWeight: 500,\n          textRendering: \"geometricPrecision\",\n        }}\n      >\n        <div\n          style={{\n            opacity: enter,\n            transform: `translateY(${(1 - enter) * 12 * s}px)`,\n            fontSize: 28 * s,\n            letterSpacing: \"-0.01em\",\n          }}\n        >\n          <SplitButton\n            t={t}\n            width={w * s}\n            height={H * s}\n            background={bg}\n            press={press}\n          >\n            {row(idle.y, idle.opacity, label)}\n            {row(\n              busy.y,\n              busy.opacity,\n              <>\n                <svg\n                  viewBox=\"0 0 24 24\"\n                  width={icon}\n                  height={icon}\n                  style={{ rotate: `${frame * 12}deg` }}\n                >\n                  <title>Pending</title>\n                  <circle\n                    cx={12}\n                    cy={12}\n                    r={9}\n                    fill=\"none\"\n                    stroke={t.primaryForeground}\n                    strokeOpacity={0.3}\n                    strokeWidth={2.6}\n                  />\n                  <path\n                    d=\"M12 3 A9 9 0 0 1 21 12\"\n                    fill=\"none\"\n                    stroke={t.primaryForeground}\n                    strokeWidth={2.6}\n                    strokeLinecap=\"round\"\n                  />\n                </svg>\n                {pending}\n              </>,\n            )}\n            {row(\n              ok.y,\n              ok.opacity,\n              <>\n                <svg viewBox=\"0 0 24 24\" width={icon} height={icon}>\n                  <title>Done</title>\n                  <path\n                    d=\"M5 12.5 L10 17 L19 7.5\"\n                    fill=\"none\"\n                    stroke={t.primaryForeground}\n                    strokeWidth={2.8}\n                    strokeLinecap=\"round\"\n                    strokeLinejoin=\"round\"\n                    pathLength={1}\n                    strokeDasharray={1}\n                    strokeDashoffset={1 - tick}\n                  />\n                </svg>\n                {done}\n              </>,\n            )}\n          </SplitButton>\n        </div>\n      </AbsoluteFill>\n    </CursorTrack>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/snap-cn/button-press.tsx"
    }
  ],
  "docs": "Docs and props: https://snapcn.dev/docs/cursor/button-press?ref=cli — label, pending and done are the three states; width is fixed so the button never reflows. 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"
}
