{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cta-button",
  "title": "CTA button",
  "description": "A glass label pill and an accent disc touching at one point; on hover the pair lifts, the disc swallows its ring, the accent washes across the label and the arrow loops through the disc.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/items/cta-button/cta-button.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { motion, useReducedMotionConfig, type Variants } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * `glass` sits on a light page: an ink capsule at 72%, a lime accent, and a\n * backdrop blur that bites on whatever is behind it. `sheet` is its twin on a\n * solid ink surface (an open menu, a dark panel): a faint on-solid tint where\n * the other is glass, on-solid where the other is lime, and no blur, because\n * there is nothing behind flat ink to pick up.\n */\nexport type CtaTone = \"glass\" | \"sheet\";\n\n/** `lg` is the hero pair (40px tall), `sm` the nav pair (36px). */\nexport type CtaSize = \"sm\" | \"lg\";\n\n/** One curve for the whole chain. */\nconst EASE = [0.2, 0.7, 0.2, 1] as const;\n\n/** Hover lift for the whole pair. */\nconst LIFT = { type: \"spring\", stiffness: 420, damping: 28 } as const;\n\n/**\n * Hover runs as a chain, not one simultaneous change:\n *   1. the accent disc swells to swallow its own ring,\n *   2. only then does the accent wash leftward across the label capsule,\n *   3. the label flips once the wash is under it,\n *   4. the arrow darts out and back while the wash travels.\n * Leaving hover reverses everything at once, quickly.\n */\nconst CTA_FILL = 0.22;\nconst CTA_WASH = 0.38;\n\n/** The lit top rim that sells glass. Kept through hover so both halves keep their edge. */\nconst LIP = \"inset 0 1px 0 color-mix(in oklab, var(--on-solid) 30%, transparent)\";\n\nconst TONES = {\n  glass: {\n    pill: \"color-mix(in oklab, var(--ink) 72%, transparent)\",\n    accent: \"var(--lime)\",\n    /** The surface the disc sits on, so its ring reads as background showing through. */\n    ringOn: \"color-mix(in oklab, var(--ink) 72%, transparent)\",\n    labelRest: \"var(--on-solid)\",\n    labelHover: \"var(--on-lime)\",\n    arrow: \"var(--on-lime)\",\n    blur: \"backdrop-blur-md\",\n    focus: \"var(--ink)\",\n  },\n  sheet: {\n    pill: \"color-mix(in oklab, var(--on-solid) 8%, transparent)\",\n    accent: \"var(--on-solid)\",\n    ringOn: \"var(--ink)\",\n    labelRest: \"var(--on-solid)\",\n    labelHover: \"var(--ink)\",\n    arrow: \"var(--ink)\",\n    blur: \"\",\n    focus: \"var(--on-solid)\",\n  },\n} as const;\n\n/**\n * The disc is permanently accent-coloured with its ring drawn as an inset\n * shadow that shrinks to nothing: one antialiased edge, so no dark hairline\n * can peek out from under it. The ring width travels as a CSS variable so the\n * shadow's colour can stay a theme token.\n */\nconst disc: Variants = {\n  rest: { \"--cta-ring\": \"3px\", transition: { duration: 0.25, ease: EASE } },\n  hover: { \"--cta-ring\": \"0px\", transition: { duration: CTA_FILL, ease: EASE } },\n};\n\n/** Resting glass fades once the wash has arrived, so no dark pixels bleed at the rim. */\nconst underlay: Variants = {\n  rest: { opacity: 1, transition: { duration: 0.2 } },\n  hover: { opacity: 0, transition: { duration: CTA_WASH, delay: CTA_FILL } },\n};\n\n/**\n * The wash block is twice the pill's width, anchored at its right edge, and\n * settles 20% past the left edge rather than flush: flush, two independently\n * antialiased curves leave a hairline at the arc. Travel is a transform, which\n * composites on the GPU, rather than `left`, which Safari stutters on behind\n * the pill's backdrop blur.\n */\nfunction washVariants(reduce: boolean): Variants {\n  return {\n    rest: { x: \"0%\", transition: { duration: reduce ? 0 : 0.28, ease: EASE, type: \"tween\" } },\n    hover: {\n      x: \"-60%\",\n      transition: reduce\n        ? { duration: 0 }\n        : { duration: CTA_WASH, ease: EASE, delay: CTA_FILL, type: \"tween\" },\n    },\n  };\n}\n\n/** The label's colour is a mix between its two tokens; the mix is what animates. */\nconst label: Variants = {\n  rest: { \"--cta-label\": \"0%\", transition: { duration: 0.2 } },\n  hover: { \"--cta-label\": \"100%\", transition: { duration: 0.18, delay: CTA_FILL + 0.1 } },\n};\n\n/**\n * The arrow exits right, then re-enters from the left. The jump happens while\n * it is outside the disc, which clips it, so the eye reads one arrow\n * travelling through. `travel` clears half the disc plus half the icon. The\n * small pair fires on the hover frame: at nav scale the wait reads as lag.\n */\nfunction arrowVariants(travel: number, delay: number, reduce: boolean): Variants {\n  if (reduce) return { rest: { x: 0 }, hover: { x: 0 } };\n  return {\n    rest: { x: 0, transition: { duration: 0.25, ease: EASE, type: \"tween\" } },\n    hover: {\n      x: [0, travel, -travel, 0],\n      transition: {\n        type: \"tween\",\n        duration: delay ? 0.7 : 0.6,\n        delay,\n        times: [0, 0.45, 0.4501, 1],\n        ease: EASE,\n      },\n    },\n  };\n}\n\n/**\n * The two capsules: a label pill and a disc meeting at a single tangent point,\n * never merged into one shape. The lens-shaped slivers above and below where\n * they touch are the whole point; do not close the gap.\n */\nfunction CtaBody({\n  label: text,\n  tone,\n  size,\n  reduce,\n}: {\n  label: React.ReactNode;\n  tone: CtaTone;\n  size: CtaSize;\n  reduce: boolean;\n}) {\n  const t = TONES[tone];\n  const large = size === \"lg\";\n  const arrow = arrowVariants(large ? 26 : 22, large ? CTA_FILL : 0, reduce);\n\n  return (\n    <>\n      {/* The blur lives on the underlay, not the pill: WebKit fails to paint a\n          transformed child inside a backdrop-filter element, which made the\n          wash vanish on Safari. */}\n      <span\n        className={cn(\n          \"relative flex items-center overflow-hidden rounded-full ring-1 ring-on-solid/25\",\n          large ? \"h-10 px-6\" : \"h-9 px-4 sm:px-5\",\n        )}\n        style={{ boxShadow: LIP }}\n      >\n        <motion.span\n          aria-hidden=\"true\"\n          variants={underlay}\n          className={cn(\"pointer-events-none absolute inset-0 rounded-full\", t.blur)}\n          style={{ backgroundColor: t.pill }}\n        />\n        {/* Only the rounded leading edge is ever on screen. The 2px vertical\n            bleed is load-bearing. */}\n        <motion.span\n          aria-hidden=\"true\"\n          variants={washVariants(reduce)}\n          className=\"absolute -inset-y-[2px] left-full block w-[200%] rounded-l-full\"\n          style={{ backgroundColor: t.accent }}\n        />\n        <motion.span\n          variants={label}\n          className={cn(\n            \"relative z-10 font-body font-medium whitespace-nowrap\",\n            large ? \"text-sm\" : \"text-xs sm:text-sm\",\n          )}\n          style={{\n            color: `color-mix(in oklab, ${t.labelHover} var(--cta-label, 0%), ${t.labelRest})`,\n          }}\n        >\n          {text}\n        </motion.span>\n      </span>\n\n      {/* overflow-hidden sells the arrow's loop-through: it vanishes at the rim\n          instead of drifting outside the circle. */}\n      <motion.span\n        variants={disc}\n        className={cn(\n          \"relative flex items-center justify-center overflow-hidden rounded-full\",\n          large ? \"size-10\" : \"size-9\",\n        )}\n        style={{\n          backgroundColor: t.accent,\n          color: t.arrow,\n          boxShadow: `inset 0 0 0 var(--cta-ring, 3px) ${t.ringOn}, ${LIP}`,\n        }}\n      >\n        <motion.span variants={arrow} className=\"flex\">\n          <svg\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth=\"1.75\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            className={large ? \"size-4\" : \"size-[15px]\"}\n            aria-hidden=\"true\"\n          >\n            <path d=\"M5 12h14\" />\n            <path d=\"m13 6 6 6-6 6\" />\n          </svg>\n        </motion.span>\n      </motion.span>\n    </>\n  );\n}\n\n/** On a touch screen the hit area grows to at least 44px without changing the look. */\nconst shell =\n  \"relative inline-flex cursor-pointer items-center focus-visible:outline-2 focus-visible:outline-offset-4 pointer-coarse:after:absolute pointer-coarse:after:top-1/2 pointer-coarse:after:left-1/2 pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 pointer-coarse:after:-translate-1/2 pointer-coarse:after:content-['']\";\n\nexport interface CtaButtonProps {\n  /** Label on the pill. Defaults to \"Get Started\". */\n  label?: React.ReactNode;\n  tone?: CtaTone;\n  size?: CtaSize;\n  /** Given, the CTA renders as an `<a>`; otherwise as a `<button>`. */\n  href?: string;\n  /** Button only. Defaults to \"submit\": the one action per form that submits it. */\n  type?: \"submit\" | \"button\";\n  /** Button only. Drops the hover chain entirely rather than running it on a dimmed capsule. */\n  disabled?: boolean;\n  onClick?: React.MouseEventHandler<HTMLElement>;\n  /** Accessible name, when the label alone is not enough. */\n  \"aria-label\"?: string;\n  \"aria-describedby\"?: string;\n  className?: string;\n}\n\n/**\n * The signature call to action: a label pill and an accent disc touching at\n * one point. On hover the pair lifts, the disc swallows its ring, the accent\n * washes across the label, and the arrow loops through the disc. As a link\n * with `href`, as a button without.\n */\nexport function CtaButton({\n  label: text = \"Get Started\",\n  tone = \"glass\",\n  size = \"lg\",\n  href,\n  type = \"submit\",\n  disabled = false,\n  onClick,\n  className,\n  ...aria\n}: CtaButtonProps) {\n  const reduce = useReducedMotionConfig() ?? false;\n  const live = href !== undefined || !disabled;\n  const tap = size === \"lg\" ? 0.97 : 0.96;\n\n  const motionProps = {\n    initial: \"rest\",\n    animate: \"rest\",\n    whileHover: live ? \"hover\" : undefined,\n    whileTap: live && !reduce ? { scale: tap } : undefined,\n    variants: { rest: { y: 0 }, hover: { y: reduce ? 0 : -2 } },\n    transition: LIFT,\n    style: { outlineColor: TONES[tone].focus },\n  } as const;\n\n  const body = <CtaBody label={text} tone={tone} size={size} reduce={reduce} />;\n\n  if (href !== undefined) {\n    return (\n      <motion.a href={href} onClick={onClick} className={cn(shell, className)} {...aria} {...motionProps}>\n        {body}\n      </motion.a>\n    );\n  }\n\n  return (\n    <motion.button\n      type={type}\n      disabled={disabled}\n      onClick={onClick}\n      className={cn(shell, \"disabled:cursor-not-allowed disabled:opacity-60\", className)}\n      {...aria}\n      {...motionProps}\n    >\n      {body}\n    </motion.button>\n  );\n}\n\nexport default CtaButton;\n",
      "type": "registry:ui"
    }
  ],
  "meta": {
    "kind": "primitive",
    "whenToUse": "Use for the single action that matters on a public or marketing screen (Get started, Go to app, Log in), one per screen. Do not use inside a dense app screen or for secondary actions; the effect is a signature and wears out when repeated.",
    "origin": {
      "project": "180Connect",
      "path": "src/components/brand/brand-cta.tsx"
    },
    "credits": []
  },
  "type": "registry:ui"
}
