{"$schema":"https:\/\/ui.particle.academy\/schema\/registry-item.json","name":"button","type":"registry:ui","title":"Button","description":"The workhorse button \u2014 colors, states, icons, emoji, avatar, badge, loading. (formerly Action)","package":"react-fancy","dependencies":[],"registryDependencies":["icon"],"files":[{"path":"components\/fancy\/button\/Button.colors.ts","content":"\/\/ AUTO-GENERATED by scripts\/gen-button-colors \u2014 do not edit by hand.\r\n\/\/ Literal Tailwind class strings for every color in the v4 palette so the\r\n\/\/ scanner can detect them in the published dist (template-built names are\r\n\/\/ invisible to Tailwind's JIT). Regenerate after changing the palette.\r\nimport type { Color } from \"..\/..\/utils\/types\";\r\n\r\nexport const buttonColorClasses: Record<Color, string> = {\r\n  slate: \"bg-slate-500 text-white border border-slate-600 hover:bg-slate-600 dark:bg-slate-600 dark:border-slate-500 dark:hover:bg-slate-500\",\r\n  gray: \"bg-gray-500 text-white border border-gray-600 hover:bg-gray-600 dark:bg-gray-600 dark:border-gray-500 dark:hover:bg-gray-500\",\r\n  zinc: \"bg-zinc-500 text-white border border-zinc-600 hover:bg-zinc-600 dark:bg-zinc-600 dark:border-zinc-500 dark:hover:bg-zinc-500\",\r\n  neutral: \"bg-neutral-500 text-white border border-neutral-600 hover:bg-neutral-600 dark:bg-neutral-600 dark:border-neutral-500 dark:hover:bg-neutral-500\",\r\n  stone: \"bg-stone-500 text-white border border-stone-600 hover:bg-stone-600 dark:bg-stone-600 dark:border-stone-500 dark:hover:bg-stone-500\",\r\n  red: \"bg-red-500 text-white border border-red-600 hover:bg-red-600 dark:bg-red-600 dark:border-red-500 dark:hover:bg-red-500\",\r\n  orange: \"bg-orange-500 text-white border border-orange-600 hover:bg-orange-600 dark:bg-orange-600 dark:border-orange-500 dark:hover:bg-orange-500\",\r\n  amber: \"bg-amber-400 text-amber-950 border border-amber-500 hover:bg-amber-500 dark:bg-amber-300 dark:text-amber-950 dark:border-amber-400 dark:hover:bg-amber-400\",\r\n  yellow: \"bg-yellow-400 text-yellow-950 border border-yellow-500 hover:bg-yellow-500 dark:bg-yellow-300 dark:text-yellow-950 dark:border-yellow-400 dark:hover:bg-yellow-400\",\r\n  lime: \"bg-lime-400 text-lime-950 border border-lime-500 hover:bg-lime-500 dark:bg-lime-300 dark:text-lime-950 dark:border-lime-400 dark:hover:bg-lime-400\",\r\n  green: \"bg-green-500 text-white border border-green-600 hover:bg-green-600 dark:bg-green-600 dark:border-green-500 dark:hover:bg-green-500\",\r\n  emerald: \"bg-emerald-500 text-white border border-emerald-600 hover:bg-emerald-600 dark:bg-emerald-600 dark:border-emerald-500 dark:hover:bg-emerald-500\",\r\n  teal: \"bg-teal-500 text-white border border-teal-600 hover:bg-teal-600 dark:bg-teal-600 dark:border-teal-500 dark:hover:bg-teal-500\",\r\n  cyan: \"bg-cyan-500 text-white border border-cyan-600 hover:bg-cyan-600 dark:bg-cyan-600 dark:border-cyan-500 dark:hover:bg-cyan-500\",\r\n  sky: \"bg-sky-500 text-white border border-sky-600 hover:bg-sky-600 dark:bg-sky-600 dark:border-sky-500 dark:hover:bg-sky-500\",\r\n  blue: \"bg-blue-500 text-white border border-blue-600 hover:bg-blue-600 dark:bg-blue-600 dark:border-blue-500 dark:hover:bg-blue-500\",\r\n  indigo: \"bg-indigo-500 text-white border border-indigo-600 hover:bg-indigo-600 dark:bg-indigo-600 dark:border-indigo-500 dark:hover:bg-indigo-500\",\r\n  violet: \"bg-violet-500 text-white border border-violet-600 hover:bg-violet-600 dark:bg-violet-600 dark:border-violet-500 dark:hover:bg-violet-500\",\r\n  purple: \"bg-purple-500 text-white border border-purple-600 hover:bg-purple-600 dark:bg-purple-600 dark:border-purple-500 dark:hover:bg-purple-500\",\r\n  fuchsia: \"bg-fuchsia-500 text-white border border-fuchsia-600 hover:bg-fuchsia-600 dark:bg-fuchsia-600 dark:border-fuchsia-500 dark:hover:bg-fuchsia-500\",\r\n  pink: \"bg-pink-500 text-white border border-pink-600 hover:bg-pink-600 dark:bg-pink-600 dark:border-pink-500 dark:hover:bg-pink-500\",\r\n  rose: \"bg-rose-500 text-white border border-rose-600 hover:bg-rose-600 dark:bg-rose-600 dark:border-rose-500 dark:hover:bg-rose-500\",\r\n};\r\n\r\nexport const buttonGhostClasses: Record<Color, string> = {\r\n  slate: \"bg-transparent text-slate-600 hover:bg-slate-50 dark:text-slate-400 dark:hover:bg-slate-950\/50\",\r\n  gray: \"bg-transparent text-gray-600 hover:bg-gray-50 dark:text-gray-400 dark:hover:bg-gray-950\/50\",\r\n  zinc: \"bg-transparent text-zinc-600 hover:bg-zinc-50 dark:text-zinc-400 dark:hover:bg-zinc-950\/50\",\r\n  neutral: \"bg-transparent text-neutral-600 hover:bg-neutral-50 dark:text-neutral-400 dark:hover:bg-neutral-950\/50\",\r\n  stone: \"bg-transparent text-stone-600 hover:bg-stone-50 dark:text-stone-400 dark:hover:bg-stone-950\/50\",\r\n  red: \"bg-transparent text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-950\/50\",\r\n  orange: \"bg-transparent text-orange-600 hover:bg-orange-50 dark:text-orange-400 dark:hover:bg-orange-950\/50\",\r\n  amber: \"bg-transparent text-amber-600 hover:bg-amber-50 dark:text-amber-400 dark:hover:bg-amber-950\/50\",\r\n  yellow: \"bg-transparent text-yellow-600 hover:bg-yellow-50 dark:text-yellow-400 dark:hover:bg-yellow-950\/50\",\r\n  lime: \"bg-transparent text-lime-600 hover:bg-lime-50 dark:text-lime-400 dark:hover:bg-lime-950\/50\",\r\n  green: \"bg-transparent text-green-600 hover:bg-green-50 dark:text-green-400 dark:hover:bg-green-950\/50\",\r\n  emerald: \"bg-transparent text-emerald-600 hover:bg-emerald-50 dark:text-emerald-400 dark:hover:bg-emerald-950\/50\",\r\n  teal: \"bg-transparent text-teal-600 hover:bg-teal-50 dark:text-teal-400 dark:hover:bg-teal-950\/50\",\r\n  cyan: \"bg-transparent text-cyan-600 hover:bg-cyan-50 dark:text-cyan-400 dark:hover:bg-cyan-950\/50\",\r\n  sky: \"bg-transparent text-sky-600 hover:bg-sky-50 dark:text-sky-400 dark:hover:bg-sky-950\/50\",\r\n  blue: \"bg-transparent text-blue-600 hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-blue-950\/50\",\r\n  indigo: \"bg-transparent text-indigo-600 hover:bg-indigo-50 dark:text-indigo-400 dark:hover:bg-indigo-950\/50\",\r\n  violet: \"bg-transparent text-violet-600 hover:bg-violet-50 dark:text-violet-400 dark:hover:bg-violet-950\/50\",\r\n  purple: \"bg-transparent text-purple-600 hover:bg-purple-50 dark:text-purple-400 dark:hover:bg-purple-950\/50\",\r\n  fuchsia: \"bg-transparent text-fuchsia-600 hover:bg-fuchsia-50 dark:text-fuchsia-400 dark:hover:bg-fuchsia-950\/50\",\r\n  pink: \"bg-transparent text-pink-600 hover:bg-pink-50 dark:text-pink-400 dark:hover:bg-pink-950\/50\",\r\n  rose: \"bg-transparent text-rose-600 hover:bg-rose-50 dark:text-rose-400 dark:hover:bg-rose-950\/50\",\r\n};\r\n","type":"registry:ui","target":"components\/fancy\/button\/Button.colors.ts"},{"path":"components\/fancy\/button\/Button.tsx","content":"import { forwardRef, type AnchorHTMLAttributes, type ElementType, type ForwardedRef, type ReactNode } from \"react\";\nimport { cn } from \"..\/..\/utils\/cn\";\nimport { sanitizeHref } from \"..\/..\/utils\/sanitize\";\nimport { resolve } from \"..\/..\/data\/emoji-utils\";\nimport { Icon } from \"..\/Icon\/Icon\";\nimport type { ButtonProps } from \".\/Button.types\";\nimport type { ButtonColor, Size } from \"..\/..\/utils\/types\";\nimport {\n  buttonColorClasses as colorClasses,\n  buttonGhostClasses as ghostColorClasses,\n} from \".\/Button.colors\";\n\nconst iconSizeMap: Record<Size, \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\"> = {\n  xs: \"xs\",\n  sm: \"xs\",\n  md: \"sm\",\n  lg: \"md\",\n  xl: \"lg\",\n};\n\nconst alertIconSizeMap: Record<Size, \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\"> = {\n  xs: \"xs\",\n  sm: \"xs\",\n  md: \"xs\",\n  lg: \"sm\",\n  xl: \"sm\",\n};\n\n\/\/ ---------------------------------------------------------------------------\n\/\/ Color classes\n\/\/\n\/\/ Solid + ghost color maps live in .\/Button.colors (AUTO-GENERATED, all 22\n\/\/ Tailwind v4 hues as literal strings). The state-color classes below\n\/\/ (default\/active\/checked\/warn) branch on flags, not specific colors.\n\/\/ ---------------------------------------------------------------------------\n\nconst defaultClasses =\n  \"bg-white text-zinc-700 border border-zinc-200 hover:bg-zinc-50 hover:border-zinc-300 dark:bg-zinc-800 dark:text-zinc-300 dark:border-zinc-700 dark:hover:bg-zinc-700\";\n\nconst activeClasses =\n  \"bg-blue-500 text-white border border-blue-600 hover:bg-blue-600 dark:bg-blue-600 dark:border-blue-500 dark:hover:bg-blue-500\";\n\nconst checkedClasses =\n  \"bg-emerald-500 text-white border border-emerald-600 hover:bg-emerald-600 dark:bg-emerald-600 dark:border-emerald-500 dark:hover:bg-emerald-500\";\n\nconst warnClasses =\n  \"bg-amber-50 text-amber-700 border border-amber-200 hover:bg-amber-100 dark:bg-amber-900\/30 dark:text-amber-300 dark:border-amber-700 dark:hover:bg-amber-900\/50\";\n\nconst ghostDefaultClasses =\n  \"bg-transparent text-zinc-700 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800\";\n\nconst ghostActiveClasses =\n  \"bg-blue-50 text-blue-700 hover:bg-blue-100 dark:bg-blue-950\/50 dark:text-blue-300 dark:hover:bg-blue-950\";\n\nconst ghostCheckedClasses =\n  \"bg-emerald-50 text-emerald-700 hover:bg-emerald-100 dark:bg-emerald-950\/50 dark:text-emerald-300 dark:hover:bg-emerald-950\";\n\nconst ghostWarnClasses =\n  \"bg-transparent text-amber-700 hover:bg-amber-50 dark:text-amber-300 dark:hover:bg-amber-950\/50\";\n\n\/\/ ---------------------------------------------------------------------------\n\/\/ Size helpers\n\/\/ ---------------------------------------------------------------------------\n\nconst horizontalSize: Record<Size, string> = {\n  xs: \"px-2 py-1 text-xs\",\n  sm: \"px-2 py-1 text-xs\",\n  md: \"px-3 py-1.5 text-sm\",\n  lg: \"px-4 py-2.5 text-base\",\n  xl: \"px-5 py-3 text-lg\",\n};\n\nconst circleSize: Record<Size, string> = {\n  xs: \"w-7 h-7 text-xs\",\n  sm: \"w-8 h-8 text-xs\",\n  md: \"w-10 h-10 text-sm\",\n  lg: \"w-12 h-12 text-base\",\n  xl: \"w-14 h-14 text-lg\",\n};\n\nconst iconSize: Record<Size, string> = {\n  xs: \"w-3 h-3\",\n  sm: \"w-3 h-3\",\n  md: \"w-4 h-4\",\n  lg: \"w-5 h-5\",\n  xl: \"w-6 h-6\",\n};\n\nconst avatarSize: Record<Size, string> = {\n  xs: \"w-4 h-4\",\n  sm: \"w-4 h-4\",\n  md: \"w-5 h-5\",\n  lg: \"w-6 h-6\",\n  xl: \"w-7 h-7\",\n};\n\nconst alertIconSize: Record<Size, string> = {\n  xs: \"w-2 h-2\",\n  sm: \"w-2.5 h-2.5\",\n  md: \"w-3 h-3\",\n  lg: \"w-4 h-4\",\n  xl: \"w-4 h-4\",\n};\n\nconst badgeSize: Record<Size, string> = {\n  xs: \"text-[10px] px-1 min-w-[14px] h-3.5\",\n  sm: \"text-[10px] px-1.5 min-w-[16px] h-4\",\n  md: \"text-[11px] px-1.5 min-w-[18px] h-[18px]\",\n  lg: \"text-xs px-2 min-w-[22px] h-5\",\n  xl: \"text-xs px-2 min-w-[24px] h-5\",\n};\n\nconst externalIconOffset: Record<Size, string> = {\n  xs: \"0.75rem\",\n  sm: \"0.75rem\",\n  md: \"1rem\",\n  lg: \"1.25rem\",\n  xl: \"1.5rem\",\n};\n\n\/\/ ---------------------------------------------------------------------------\n\/\/ Helpers\n\/\/ ---------------------------------------------------------------------------\n\nfunction isColored(color?: ButtonColor, active?: boolean, checked?: boolean): boolean {\n  return !!(color || active || checked);\n}\n\nfunction getColorClasses(\n  color?: ButtonColor,\n  active?: boolean,\n  checked?: boolean,\n  warn?: boolean,\n  ghost?: boolean,\n): string {\n  if (ghost) {\n    if (color) return ghostColorClasses[color];\n    if (checked) return ghostCheckedClasses;\n    if (active) return ghostActiveClasses;\n    if (warn) return ghostWarnClasses;\n    return ghostDefaultClasses;\n  }\n  if (color) return colorClasses[color];\n  if (checked) return checkedClasses;\n  if (active) return activeClasses;\n  if (warn) return warnClasses;\n  return defaultClasses;\n}\n\nfunction getIconColorClasses(\n  color?: ButtonColor,\n  active?: boolean,\n  checked?: boolean,\n  warn?: boolean,\n  ghost?: boolean,\n): string {\n  if (ghost) {\n    \/\/ Ghost variant: icon inherits currentColor from the button text\n    return \"text-current\";\n  }\n  if (color || active || checked) return \"text-white\";\n  if (warn) return \"text-amber-600 dark:text-amber-400\";\n  return \"text-zinc-500 dark:text-zinc-400\";\n}\n\nfunction getBadgeClasses(\n  color?: ButtonColor,\n  active?: boolean,\n  checked?: boolean,\n  warn?: boolean,\n  ghost?: boolean,\n): string {\n  if (ghost) {\n    return \"bg-current\/10 text-current\";\n  }\n  if (isColored(color, active, checked)) return \"bg-white\/20 text-white\";\n  if (warn) return \"bg-amber-200 text-amber-800 dark:bg-amber-800 dark:text-amber-200\";\n  return \"bg-zinc-200 text-zinc-700 dark:bg-zinc-600 dark:text-zinc-200\";\n}\n\n\/\/ ---------------------------------------------------------------------------\n\/\/ Placement parsing\n\/\/ ---------------------------------------------------------------------------\n\ntype PlacementAxis = \"left\" | \"right\" | \"top\" | \"bottom\";\n\nfunction parsePlacement(iconPlace: string): { primary: PlacementAxis; secondary?: PlacementAxis } {\n  const parts = iconPlace.split(\" \") as PlacementAxis[];\n  return { primary: parts[0], secondary: parts[1] };\n}\n\nfunction isVerticalPrimary(primary: PlacementAxis): boolean {\n  return primary === \"top\" || primary === \"bottom\";\n}\n\n\/\/ ---------------------------------------------------------------------------\n\/\/ Sort parsing\n\/\/ ---------------------------------------------------------------------------\n\ntype ElementKey = \"e\" | \"i\" | \"a\" | \"b\";\nconst DEFAULT_SORT = \"eiab\";\n\nfunction parseSortOrder(sort?: string): ElementKey[] {\n  const chars = (sort ?? DEFAULT_SORT).split(\"\") as ElementKey[];\n  const valid: ElementKey[] = [];\n  const seen = new Set<string>();\n  for (const c of chars) {\n    if (\"eiab\".includes(c) && !seen.has(c)) {\n      valid.push(c);\n      seen.add(c);\n    }\n  }\n  \/\/ Add missing keys in default order\n  for (const c of DEFAULT_SORT.split(\"\") as ElementKey[]) {\n    if (!seen.has(c)) valid.push(c);\n  }\n  return valid;\n}\n\n\/\/ ---------------------------------------------------------------------------\n\/\/ Component\n\/\/ ---------------------------------------------------------------------------\n\nexport const Button = forwardRef<HTMLButtonElement, ButtonProps>(\n  (\n    {\n      children,\n      className,\n      variant = \"default\",\n      color,\n      size = \"md\",\n      active,\n      checked,\n      warn,\n      alert: alertProp,\n      icon,\n      iconTrailing,\n      iconPlace = \"left\",\n      alertIcon,\n      alertIconTrailing,\n      emoji,\n      emojiTrailing,\n      avatar,\n      avatarTrailing,\n      badge,\n      badgeTrailing,\n      sort,\n      loading = false,\n      disabled,\n      href,\n      as: As,\n      responsive,\n      labelClassName,\n      ...props\n    },\n    ref,\n  ) => {\n    const isCircle = variant === \"circle\";\n    const isGhost = variant === \"ghost\";\n    const { primary, secondary } = parsePlacement(iconPlace);\n    const isVertical = isVerticalPrimary(primary);\n    const isCompound = !!secondary;\n    const needsWrapper = isVertical || isCompound;\n    const isReversed = iconPlace === \"right\";\n\n    \/\/ -----------------------------------------------------------------------\n    \/\/ Sizing \u2014 always use horizontalSize for the button itself\n    \/\/ -----------------------------------------------------------------------\n    const sizeClass = isCircle ? circleSize[size] : horizontalSize[size];\n\n    \/\/ -----------------------------------------------------------------------\n    \/\/ Build decorative elements\n    \/\/ -----------------------------------------------------------------------\n    const iconColorCls = getIconColorClasses(color, active, checked, warn, isGhost);\n\n    const buildElement = (key: ElementKey, trailing: boolean): ReactNode => {\n      switch (key) {\n        case \"e\": {\n          const slug = trailing ? emojiTrailing : emoji;\n          if (!slug) return null;\n          const char = resolve(slug);\n          if (!char) return null;\n          return (\n            <span key={`emoji-${trailing ? \"t\" : \"l\"}`} data-action-emoji>\n              {char}\n            <\/span>\n          );\n        }\n        case \"i\": {\n          if (loading && !trailing) {\n            return (\n              <span\n                key=\"spinner\"\n                className={cn(\"animate-spin border-2 border-current border-t-transparent rounded-full\", iconSize[size])}\n              \/>\n            );\n          }\n          const iconSlug = trailing ? iconTrailing : icon;\n          if (!iconSlug) return null;\n          \/\/ For vertical\/compound placements, icons render outside the button \u2014 not inside\n          if (needsWrapper) return null;\n          return (\n            <span\n              key={`icon-${trailing ? \"t\" : \"l\"}`}\n              className={cn(\"inline-flex items-center flex-shrink-0\", iconColorCls)}\n            >\n              <Icon name={iconSlug} size={iconSizeMap[size]} \/>\n            <\/span>\n          );\n        }\n        case \"a\": {\n          if (!avatar) return null;\n          const isTrailing = avatarTrailing ?? trailing;\n          if (isTrailing !== trailing) return null;\n          return (\n            <img\n              key=\"avatar\"\n              src={avatar}\n              alt=\"\"\n              className={cn(avatarSize[size], \"rounded-full object-cover flex-shrink-0\")}\n              data-action-avatar\n            \/>\n          );\n        }\n        case \"b\": {\n          if (!badge) return null;\n          const isTrailing = badgeTrailing ?? trailing;\n          if (isTrailing !== trailing) return null;\n          return (\n            <span\n              key=\"badge\"\n              className={cn(\n                \"inline-flex items-center justify-center font-medium rounded-full\",\n                badgeSize[size],\n                getBadgeClasses(color, active, checked, warn, isGhost),\n              )}\n              data-action-badge\n            >\n              {badge}\n            <\/span>\n          );\n        }\n        default:\n          return null;\n      }\n    };\n\n    \/\/ Alert icon element\n    const renderAlertIcon = (trailing: boolean): ReactNode => {\n      if (!alertIcon) return null;\n      const isTrailing = alertIconTrailing ?? false;\n      if (isTrailing !== trailing) return null;\n      const alertIconEl = <Icon name={alertIcon} size={alertIconSizeMap[size]} \/>;\n      return (\n        <span\n          key=\"alert-icon\"\n          className=\"relative inline-flex flex-shrink-0\"\n          data-action-alert\n        >\n          <span className=\"text-red-500 dark:text-red-400 animate-pulse\">\n            {alertIconEl}\n          <\/span>\n          <span className=\"absolute inset-0 flex items-center justify-center text-red-400 dark:text-red-300 animate-ping opacity-75\">\n            {alertIconEl}\n          <\/span>\n        <\/span>\n      );\n    };\n\n    const sortOrder = parseSortOrder(sort);\n\n    const leadingElements: ReactNode[] = [];\n    const trailingElements: ReactNode[] = [];\n\n    for (const key of sortOrder) {\n      const leading = buildElement(key, false);\n      if (leading) leadingElements.push(leading);\n      const trailing = buildElement(key, true);\n      if (trailing) trailingElements.push(trailing);\n    }\n\n    \/\/ Insert alert icon\n    const leadingAlert = renderAlertIcon(false);\n    if (leadingAlert) leadingElements.push(leadingAlert);\n    const trailingAlert = renderAlertIcon(true);\n    if (trailingAlert) trailingElements.push(trailingAlert);\n\n    \/\/ -----------------------------------------------------------------------\n    \/\/ Classes\n    \/\/ -----------------------------------------------------------------------\n    const classes = cn(\n      \/\/ `text-left` overrides the UA <button> center default so a wrapped,\n      \/\/ width-constrained multi-word label stacks left-aligned. Overridable \u2014\n      \/\/ twMerge lets a `text-center` \/ `text-right` in `className` win.\n      \"inline-flex items-center justify-center text-left font-medium transition-all duration-200\",\n      \"focus:outline-none focus:ring-2 focus:ring-offset-1\",\n      \"disabled:opacity-50 disabled:cursor-not-allowed\",\n      isCircle ? \"rounded-full\" : \"rounded-lg\",\n      sizeClass,\n      getColorClasses(color, active, checked, warn, isGhost),\n      !isCircle && !needsWrapper && \"gap-2\",\n      !isCircle && needsWrapper && \"gap-2\",\n      isReversed && !needsWrapper && \"flex-row-reverse\",\n      alertProp && \"animate-pulse\",\n      className,\n    );\n\n    \/\/ -----------------------------------------------------------------------\n    \/\/ Content\n    \/\/ -----------------------------------------------------------------------\n    const content = isCircle ? (\n      <>\n        {loading ? (\n          <span\n            className={cn(\n              \"animate-spin border-2 border-current border-t-transparent rounded-full\",\n              iconSize[size],\n            )}\n          \/>\n        ) : (\n          icon ? <Icon name={icon} size={iconSizeMap[size]} \/> : children\n        )}\n      <\/>\n    ) : (\n      <>\n        {leadingElements}\n        {children != null && (\n          <span\n            data-react-fancy-button-label=\"\"\n            className={cn(\n              \/\/ A button label must stay on one line. When a host flex layout\n              \/\/ squeezes the button, `whitespace-nowrap` keeps the label and any\n              \/\/ trailing icon\/arrow together instead of wrapping and stacking the\n              \/\/ icon below it (the classic \"send\" over \"\u2192\" break). `min-w-0` keeps\n              \/\/ the span shrinkable so `truncate`\/ellipsis still works; multi-line\n              \/\/ wrapping is opt-in via `labelClassName` (\"whitespace-normal\") or\n              \/\/ the `responsive` prop.\n              \"min-w-0 whitespace-nowrap\",\n              \/\/ `responsive` collapses to icon-only when squeezed, while keeping\n              \/\/ the label readable to screen readers.\n              responsive && \"sr-only sm:not-sr-only\",\n              labelClassName,\n            )}\n          >\n            {children}\n          <\/span>\n        )}\n        {trailingElements}\n      <\/>\n    );\n\n    \/\/ -----------------------------------------------------------------------\n    \/\/ Build the button element\n    \/\/ -----------------------------------------------------------------------\n    const safeHref = sanitizeHref(href);\n    \/\/ Anchor mode renders `as` when provided (e.g. a router's <Link> for\n    \/\/ client-side navigation \u2014 Inertia, React Router, Next). Previously `as`\n    \/\/ fell through ...props onto the <a> where React silently DROPS a\n    \/\/ function-valued unknown attribute \u2014 so `<Button as={Link}>` looked fine\n    \/\/ but did a full page load. `as` only applies in anchor mode; without an\n    \/\/ href (or when disabled) the plain <button> renders as always.\n    const Anchor = (As ?? \"a\") as ElementType;\n    const buttonEl = safeHref && !disabled ? (\n      \/\/ Anchor mode forwards the same rest props as the <button> branch so\n      \/\/ onClick \/ target \/ rel \/ ref \/ ARIA \/ data-* reach the <a> (issue #7).\n      <Anchor\n        ref={ref as ForwardedRef<HTMLAnchorElement>}\n        href={safeHref}\n        className={classes}\n        data-react-fancy-button=\"\"\n        data-react-fancy-action=\"\"\n        {...(props as AnchorHTMLAttributes<HTMLAnchorElement>)}\n      >\n        {content}\n      <\/Anchor>\n    ) : (\n      <button\n        ref={ref}\n        className={classes}\n        disabled={disabled || loading}\n        data-react-fancy-button=\"\"\n        data-react-fancy-action=\"\"\n        {...props}\n      >\n        {content}\n      <\/button>\n    );\n\n    \/\/ -----------------------------------------------------------------------\n    \/\/ Wrap for vertical\/compound icon placement\n    \/\/ The icon is absolutely positioned so it never shifts the button.\n    \/\/ -----------------------------------------------------------------------\n    if (needsWrapper && (icon || iconTrailing)) {\n      const iconSlug = icon ?? iconTrailing;\n      const offset = externalIconOffset[size];\n\n      \/\/ Determine position styles for the icon\n      const posStyle: React.CSSProperties = { position: \"absolute\" };\n\n      \/\/ Primary axis: which side of the button\n      if (primary === \"top\") {\n        posStyle.bottom = \"100%\";\n        posStyle.marginBottom = \"0.125rem\";\n      } else if (primary === \"bottom\") {\n        posStyle.top = \"100%\";\n        posStyle.marginTop = \"0.125rem\";\n      } else if (primary === \"left\") {\n        posStyle.right = \"100%\";\n        posStyle.marginRight = \"0.125rem\";\n      } else if (primary === \"right\") {\n        posStyle.left = \"100%\";\n        posStyle.marginLeft = \"0.125rem\";\n      }\n\n      \/\/ Secondary axis: cross-axis alignment\n      if (isVerticalPrimary(primary)) {\n        \/\/ Vertical primary \u2192 secondary controls horizontal position\n        if (secondary === \"left\") {\n          posStyle.left = \"0\";\n        } else if (secondary === \"right\") {\n          posStyle.right = \"0\";\n        } else {\n          \/\/ center (default)\n          posStyle.left = \"50%\";\n          posStyle.transform = \"translateX(-50%)\";\n        }\n      } else {\n        \/\/ Horizontal primary \u2192 secondary controls vertical position\n        if (secondary === \"top\") {\n          posStyle.top = \"0\";\n        } else if (secondary === \"bottom\") {\n          posStyle.bottom = \"0\";\n        } else {\n          \/\/ center (default)\n          posStyle.top = \"50%\";\n          posStyle.transform = \"translateY(-50%)\";\n        }\n      }\n\n      const externalIcon = (\n        <span\n          className={cn(\"absolute flex-shrink-0\", iconColorCls)}\n          style={posStyle}\n        >\n          <Icon name={iconSlug!} size={iconSizeMap[size]} \/>\n        <\/span>\n      );\n\n      return (\n        <span\n          data-react-fancy-action-group=\"\"\n          className=\"relative inline-flex\"\n        >\n          {buttonEl}\n          {externalIcon}\n        <\/span>\n      );\n    }\n\n    return buttonEl;\n  },\n);\n\nButton.displayName = \"Button\";\n\n\/**\n * @deprecated Renamed to {@link Button}. `Action` remains as an alias for\n * backward compatibility and will be removed in a future major version.\n * Prefer importing `Button` from \"@particle-academy\/react-fancy\".\n *\/\nexport const Action = Button;\n","type":"registry:ui","target":"components\/fancy\/button\/Button.tsx"},{"path":"components\/fancy\/button\/Button.types.ts","content":"import type { AnchorHTMLAttributes, ButtonHTMLAttributes, ElementType } from \"react\";\nimport type { ButtonColor, Size } from \"..\/..\/utils\/types\";\n\nexport interface ButtonProps\n  extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, \"color\"> {\n  \/** Shape\/fill variant *\/\n  variant?: \"default\" | \"circle\" | \"ghost\";\n  \/** Standalone color (overrides state colors) *\/\n  color?: ButtonColor;\n  size?: Size;\n  \/** Behavioral states *\/\n  active?: boolean;\n  checked?: boolean;\n  warn?: boolean;\n  alert?: boolean;\n  \/** Leading icon slug (resolved via Icon component, e.g. \"pencil\", \"chevron-right\") *\/\n  icon?: string;\n  \/** Trailing icon slug (convenience for right-side icon) *\/\n  iconTrailing?: string;\n  \/** Icon placement direction *\/\n  iconPlace?:\n    | \"left\" | \"right\" | \"top\" | \"bottom\"\n    | \"top left\" | \"top right\" | \"bottom left\" | \"bottom right\"\n    | \"left top\" | \"left bottom\" | \"right top\" | \"right bottom\";\n  \/** Pulsing alert icon slug *\/\n  alertIcon?: string;\n  \/** Position alert icon on trailing side *\/\n  alertIconTrailing?: boolean;\n  \/** Emoji slug (resolved via emoji-utils) *\/\n  emoji?: string;\n  \/** Trailing emoji slug *\/\n  emojiTrailing?: string;\n  \/** Avatar image URL *\/\n  avatar?: string;\n  \/** Position avatar on trailing side *\/\n  avatarTrailing?: boolean;\n  \/** Badge text *\/\n  badge?: string;\n  \/** Position badge on trailing side *\/\n  badgeTrailing?: boolean;\n  \/** Sort order of decorative elements: e=emoji, i=icon, a=avatar, b=badge *\/\n  sort?: string;\n  \/** Show loading spinner *\/\n  loading?: boolean;\n  disabled?: boolean;\n  \/** Render as anchor tag *\/\n  href?: string;\n  \/**\n   * Anchor-mode element override \u2014 the component to render INSTEAD of the\n   * plain `<a>` when `href` is set, e.g. a router's `<Link>` (Inertia, React\n   * Router, Next) so the button navigates client-side. Receives `href`,\n   * `className`, and the forwarded rest props. Ignored without `href` or when\n   * `disabled` (the plain `<button>` renders as always).\n   *\/\n  as?: ElementType;\n  \/** Anchor `target` \u2014 only applies in `href` (anchor) mode. *\/\n  target?: AnchorHTMLAttributes<HTMLAnchorElement>[\"target\"];\n  \/** Anchor `rel` \u2014 only applies in `href` (anchor) mode. *\/\n  rel?: AnchorHTMLAttributes<HTMLAnchorElement>[\"rel\"];\n  \/** Anchor `download` \u2014 only applies in `href` (anchor) mode. *\/\n  download?: AnchorHTMLAttributes<HTMLAnchorElement>[\"download\"];\n  \/**\n   * Collapse to icon-only when squeezed \u2014 injects the minimal Tailwind classes\n   * (`sr-only sm:not-sr-only`) onto the label so it stays screen-reader\n   * accessible but visually hidden on small screens. The label is also directly\n   * targetable via `data-react-fancy-button-label` for a custom breakpoint, e.g.\n   * `className=\"[&_[data-react-fancy-button-label]]:hidden md:[&_[data-react-fancy-button-label]]:inline\"`.\n   *\/\n  responsive?: boolean;\n  \/** Extra classes for the label wrapper \u2014 alignment, responsive hiding, etc. *\/\n  labelClassName?: string;\n}\n\n\/**\n * @deprecated Renamed to {@link ButtonProps}. `ActionProps` remains as an alias\n * for backward compatibility and will be removed in a future major version.\n *\/\nexport type ActionProps = ButtonProps;\n","type":"registry:ui","target":"components\/fancy\/button\/Button.types.ts"},{"path":"components\/fancy\/button\/index.ts","content":"export { Button } from \".\/Button\";\nexport type { ButtonProps } from \".\/Button.types\";\n\/** @deprecated Renamed to Button \u2014 use Button. *\/\nexport { Action } from \".\/Button\";\n\/** @deprecated Renamed to ButtonProps \u2014 use ButtonProps. *\/\nexport type { ActionProps } from \".\/Button.types\";\n","type":"registry:ui","target":"components\/fancy\/button\/index.ts"}]}