{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "arrow-button",
  "title": "Arrow button",
  "description": "A pill whose small dot blooms to fill it on hover while the label slides out and returns beside an up-right arrow in inverted colours, without the pill changing width.",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/items/arrow-button/arrow-button.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { useReducedMotionConfig } from \"motion/react\";\nimport { ArrowUpRight } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * `solid` is the hero pill: a primary fill whose dot blooms into the page\n * colour. `outline` is the navigation pill: a hairline whose dot blooms into\n * the primary fill. Each ends in the other's colours.\n */\nexport type ArrowButtonVariant = \"solid\" | \"outline\";\n\nexport type ArrowButtonSize = \"sm\" | \"md\" | \"lg\";\n\ninterface BaseProps {\n  /** The label, at rest and again beside the arrow on hover. Defaults to \"Download Now\". */\n  label?: React.ReactNode;\n  variant?: ArrowButtonVariant;\n  size?: ArrowButtonSize;\n  className?: string;\n}\n\ntype AnchorProps = BaseProps &\n  Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, \"children\" | keyof BaseProps> & {\n    /** Given, the pill renders as an `<a>`; otherwise as a `<button>`. */\n    href: string;\n  };\n\ntype ButtonProps = BaseProps &\n  Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"children\" | keyof BaseProps> & {\n    href?: undefined;\n  };\n\nexport type ArrowButtonProps = AnchorProps | ButtonProps;\n\n/**\n * Every layer carries its own text colour rather than inheriting the shell's,\n * so a project's global `a` / `a:hover` / `a:visited` colours cannot repaint\n * the label when the pill is a link.\n */\nconst VARIANTS = {\n  solid: {\n    shell: \"border-primary bg-primary\",\n    rest: \"text-primary-foreground\",\n    dot: \"bg-background\",\n    hover: \"text-foreground\",\n  },\n  outline: {\n    shell: \"border-foreground bg-transparent\",\n    rest: \"text-foreground\",\n    dot: \"bg-primary\",\n    hover: \"text-primary-foreground\",\n  },\n} as const;\n\nconst SIZES = {\n  sm: \"h-9 px-5 text-sm\",\n  md: \"h-10 px-6 text-[15px]\",\n  lg: \"h-12 px-8 text-base\",\n} as const;\n\nconst SHELL =\n  \"group/ihb relative inline-flex cursor-pointer touch-manipulation items-center justify-center overflow-hidden rounded-full border font-heading font-semibold whitespace-nowrap no-underline select-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring active:scale-[0.98] disabled:pointer-events-none disabled:opacity-50\";\n\n/**\n * A pill whose small dot blooms to fill it on hover. The resting label slides\n * out to the right and fades while the same label returns beside an up-right\n * arrow in the inverted colours. An invisible copy of that hover row stays in\n * flow, so the pill is always as wide as its widest state and never jumps.\n *\n * Hover is hover-capable pointers only (Tailwind's `hover:` is gated on\n * `(hover: hover)`), so nothing sticks after a tap; keyboard focus plays the\n * same bloom. Reduced motion swaps the colours without the bloom or slide.\n */\nexport const ArrowButton = React.forwardRef<\n  HTMLAnchorElement | HTMLButtonElement,\n  ArrowButtonProps\n>(function ArrowButton(\n  { label = \"Download Now\", variant = \"solid\", size = \"md\", className, ...props },\n  ref,\n) {\n  const reduce = useReducedMotionConfig() ?? false;\n  const v = VARIANTS[variant];\n  const timing = reduce ? \"transition-none\" : \"duration-300 ease-out motion-reduce:transition-none\";\n  const classes = cn(SHELL, v.shell, SIZES[size], className);\n\n  const body = (\n    <>\n      {/* In-flow sizer: as wide as the hover row, so the pill holds its width. */}\n      <span aria-hidden=\"true\" className=\"invisible flex shrink-0 items-center gap-2\">\n        <span>{label}</span>\n        <ArrowUpRight className=\"size-4 shrink-0\" />\n      </span>\n\n      {/* The resting label, nudged right of the dot where there is a dot to clear. */}\n      <span\n        className={cn(\n          \"absolute inset-0 z-[2] flex items-center justify-center transition-opacity group-hover/ihb:opacity-0 group-focus-visible/ihb:opacity-0\",\n          v.rest,\n          timing,\n        )}\n      >\n        <span className=\"inline-block pointer-fine:translate-x-2\">\n          <span\n            className={cn(\n              \"inline-block transition-transform group-hover/ihb:translate-x-12 group-focus-visible/ihb:translate-x-12\",\n              timing,\n            )}\n          >\n            {label}\n          </span>\n        </span>\n      </span>\n\n      {/* The hover row: the label again, with the arrow, in the bloom's colours. */}\n      <span\n        aria-hidden=\"true\"\n        className={cn(\n          \"absolute inset-0 z-[3] flex items-center justify-center gap-2 opacity-0 transition-opacity group-hover/ihb:opacity-100 group-focus-visible/ihb:opacity-100\",\n          v.hover,\n          timing,\n        )}\n      >\n        <span>{label}</span>\n        <ArrowUpRight className=\"size-4 shrink-0\" />\n      </span>\n\n      {/* The dot. Touch screens have no hover to bloom it, so they never see it. */}\n      <span\n        aria-hidden=\"true\"\n        className={cn(\n          \"absolute top-1/2 left-[15%] z-[1] size-2 -translate-y-1/2 rounded-lg transition-all pointer-coarse:hidden\",\n          \"group-hover/ihb:top-0 group-hover/ihb:left-0 group-hover/ihb:h-full group-hover/ihb:w-full group-hover/ihb:translate-y-0 group-hover/ihb:scale-[1.8]\",\n          \"group-focus-visible/ihb:top-0 group-focus-visible/ihb:left-0 group-focus-visible/ihb:h-full group-focus-visible/ihb:w-full group-focus-visible/ihb:translate-y-0 group-focus-visible/ihb:scale-[1.8]\",\n          v.dot,\n          timing,\n        )}\n      />\n    </>\n  );\n\n  if (props.href !== undefined) {\n    return (\n      <a ref={ref as React.Ref<HTMLAnchorElement>} className={classes} {...(props as Omit<AnchorProps, keyof BaseProps>)}>\n        {body}\n      </a>\n    );\n  }\n\n  const { type = \"button\", ...rest } = props as Omit<ButtonProps, keyof BaseProps>;\n  return (\n    <button ref={ref as React.Ref<HTMLButtonElement>} type={type} className={classes} {...rest}>\n      {body}\n    </button>\n  );\n});\n\nexport default ArrowButton;\n",
      "type": "registry:ui"
    }
  ],
  "meta": {
    "kind": "primitive",
    "whenToUse": "Use for a page's main outbound call to action, such as Download or Sign up: solid in the hero, outlined in the navigation. Do not use for form submits or dense app toolbars; the bloom is a marketing flourish and loses its point when repeated.",
    "origin": {
      "project": "Steeze",
      "path": "src/components/ui/interactive-hover-button.jsx"
    },
    "credits": [
      {
        "name": "Magic UI, Interactive Hover Button (dot bloom and label swap; Steeze added the in-flow sizer, outline variant and up-right arrow)",
        "url": "https://magicui.design/docs/components/arrow-button",
        "license": "MIT"
      }
    ]
  },
  "type": "registry:ui"
}
