{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "button",
  "type": "registry:ui",
  "title": "Button",
  "description": "Button is a primary interactive trigger for user actions — submit, confirm, navigate, or initiate a process.",
  "author": "Skrewww",
  "dependencies": [],
  "registryDependencies": [
    "@skrewww/foundation"
  ],
  "docs": "Host requirements (assumed already present, not installed by this command): react, react-dom.",
  "files": [
    {
      "path": "components/ui/Button.tsx",
      "content": "/** @skrewww-component button */\n\"use client\";\n\nimport {\n  forwardRef,\n  type AnchorHTMLAttributes,\n  type ButtonHTMLAttributes,\n  type ReactNode,\n} from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { RouterAnchor } from \"@/components/ui/router-navigation\";\nimport { LoadingSpinner } from \"@/components/ui/icons\";\nimport { useButtonGroupItem } from \"@/components/ui/button-group-context\";\nimport styles from \"@/components/ui/button.module.css\";\n\nexport type ButtonVariant = \"primary\" | \"secondary\" | \"danger\";\nexport type ButtonSize = \"sm\" | \"md\" | \"lg\";\n\ntype SharedButtonProps = {\n  variant?: ButtonVariant;\n  size?: ButtonSize;\n  loading?: boolean;\n  fullWidth?: boolean;\n  leadingIcon?: ReactNode;\n  trailingIcon?: ReactNode;\n  /** Visible label. For icon-only buttons, also pass `aria-label`. */\n  children: ReactNode;\n  className?: string;\n};\n\nexport type ButtonProps = SharedButtonProps &\n  Omit<ButtonHTMLAttributes<HTMLButtonElement>, keyof SharedButtonProps> & {\n    href?: never;\n    target?: never;\n    rel?: never;\n  };\n\nexport type ButtonLinkProps = SharedButtonProps &\n  Omit<AnchorHTMLAttributes<HTMLAnchorElement>, keyof SharedButtonProps | \"href\"> & {\n    href: string;\n    disabled?: boolean;\n    target?: AnchorHTMLAttributes<HTMLAnchorElement>[\"target\"];\n    rel?: string;\n  };\n\nexport type ButtonComponentProps = ButtonProps | ButtonLinkProps;\n\nconst sizeClass: Record<ButtonSize, string> = {\n  sm: styles.sizeSm,\n  md: styles.sizeMd,\n  lg: styles.sizeLg,\n};\n\nconst variantClass: Record<ButtonVariant, string> = {\n  primary: styles.primary,\n  secondary: styles.secondary,\n  danger: styles.danger,\n};\n\nfunction isExternalHref(href: string): boolean {\n  return /^(https?:)?\\/\\//.test(href) || href.startsWith(\"mailto:\") || href.startsWith(\"tel:\");\n}\n\nfunction ButtonContent({\n  loading,\n  leadingIcon,\n  trailingIcon,\n  children,\n}: Pick<SharedButtonProps, \"loading\" | \"leadingIcon\" | \"trailingIcon\" | \"children\">) {\n  return (\n    <>\n      {loading ? (\n        <span className={styles.icon} aria-hidden=\"true\">\n          <LoadingSpinner className={styles.spinner} />\n        </span>\n      ) : leadingIcon ? (\n        <span className={styles.icon} aria-hidden=\"true\">\n          {leadingIcon}\n        </span>\n      ) : null}\n      <span className={styles.label}>{children}</span>\n      {!loading && trailingIcon ? (\n        <span className={styles.icon} aria-hidden=\"true\">\n          {trailingIcon}\n        </span>\n      ) : null}\n      {loading ? <span className=\"sr-only\">Loading</span> : null}\n    </>\n  );\n}\n\nfunction ButtonLayers({\n  loading,\n  leadingIcon,\n  trailingIcon,\n  children,\n}: Pick<SharedButtonProps, \"loading\" | \"leadingIcon\" | \"trailingIcon\" | \"children\">) {\n  return (\n    <>\n      <span className={styles.visualSurface} aria-hidden=\"true\" />\n      <span className={styles.content}>\n        <ButtonContent loading={loading} leadingIcon={leadingIcon} trailingIcon={trailingIcon}>\n          {children}\n        </ButtonContent>\n      </span>\n    </>\n  );\n}\n\nfunction useButtonClasses(\n  variant: ButtonVariant,\n  size: ButtonSize,\n  fullWidth: boolean,\n  inGroup: boolean,\n  className?: string,\n) {\n  return cn(\n    styles.button,\n    variantClass[variant],\n    sizeClass[size],\n    fullWidth && styles.fullWidth,\n    inGroup && styles.inGroup,\n    className,\n  );\n}\n\nfunction DisabledLink({\n  className,\n  loading,\n  leadingIcon,\n  trailingIcon,\n  children,\n  ariaLabel,\n}: {\n  className: string;\n  loading?: boolean;\n  leadingIcon?: ReactNode;\n  trailingIcon?: ReactNode;\n  children: ReactNode;\n  ariaLabel?: string;\n}) {\n  return (\n    <span\n      role=\"link\"\n      className={className}\n      aria-disabled=\"true\"\n      aria-busy={loading || undefined}\n      aria-label={ariaLabel}\n      tabIndex={-1}\n    >\n      <ButtonLayers loading={loading} leadingIcon={leadingIcon} trailingIcon={trailingIcon}>\n        {children}\n      </ButtonLayers>\n    </span>\n  );\n}\n\nexport const Button = forwardRef<HTMLButtonElement, ButtonComponentProps>(function Button(\n  props,\n  ref,\n) {\n  const {\n    variant = \"primary\",\n    size = \"md\",\n    loading = false,\n    fullWidth = false,\n    leadingIcon,\n    trailingIcon,\n    className,\n    children,\n  } = props;\n\n  const inGroup = useButtonGroupItem();\n  const classes = useButtonClasses(variant, size, fullWidth, inGroup, className);\n  const isDisabled = Boolean(props.disabled || loading);\n\n  if (\"href\" in props && props.href) {\n    const {\n      href,\n      target,\n      rel,\n      disabled,\n      \"aria-label\": ariaLabel,\n      variant: _variant,\n      size: _size,\n      loading: _loading,\n      fullWidth: _fullWidth,\n      leadingIcon: _leadingIcon,\n      trailingIcon: _trailingIcon,\n      className: _className,\n      children: _children,\n      ...anchorRest\n    } = props;\n    const linkDisabled = Boolean(disabled || loading);\n\n    if (linkDisabled) {\n      return (\n        <DisabledLink\n          className={classes}\n          loading={loading}\n          leadingIcon={leadingIcon}\n          trailingIcon={trailingIcon}\n          ariaLabel={ariaLabel}\n        >\n          {children}\n        </DisabledLink>\n      );\n    }\n\n    const external = isExternalHref(href);\n    const resolvedRel =\n      rel ?? (target === \"_blank\" ? \"noopener noreferrer\" : undefined);\n\n    if (external) {\n      return (\n        <a\n          href={href}\n          className={classes}\n          target={target}\n          rel={resolvedRel}\n          aria-busy={loading || undefined}\n          aria-label={ariaLabel}\n          {...anchorRest}\n        >\n          <ButtonLayers\n            loading={loading}\n            leadingIcon={leadingIcon}\n            trailingIcon={trailingIcon}\n          >\n            {children}\n          </ButtonLayers>\n        </a>\n      );\n    }\n\n    return (\n      <RouterAnchor\n        href={href}\n        className={classes}\n        target={target}\n        rel={resolvedRel}\n        aria-busy={loading || undefined}\n        aria-label={ariaLabel}\n        {...anchorRest}\n      >\n        <ButtonLayers\n          loading={loading}\n          leadingIcon={leadingIcon}\n          trailingIcon={trailingIcon}\n        >\n          {children}\n        </ButtonLayers>\n      </RouterAnchor>\n    );\n  }\n\n  const {\n    type = \"button\",\n    disabled,\n    \"aria-label\": ariaLabel,\n    variant: _variant,\n    size: _size,\n    loading: _loading,\n    fullWidth: _fullWidth,\n    leadingIcon: _leadingIcon,\n    trailingIcon: _trailingIcon,\n    className: _className,\n    children: _children,\n    ...buttonRest\n  } = props as ButtonProps;\n\n  return (\n    <button\n      ref={ref}\n      type={type}\n      className={classes}\n      disabled={isDisabled}\n      aria-busy={loading || undefined}\n      aria-disabled={isDisabled || undefined}\n      aria-label={ariaLabel}\n      {...buttonRest}\n    >\n      <ButtonLayers loading={loading} leadingIcon={leadingIcon} trailingIcon={trailingIcon}>\n        {children}\n      </ButtonLayers>\n    </button>\n  );\n});\n",
      "type": "registry:ui",
      "target": "~/components/ui/Button.tsx"
    },
    {
      "path": "components/ui/button.module.css",
      "content": ".button {\n  /* Explicit, not assumed: this repo's docs shell gets border-box from\n     Tailwind's Preflight, but Button must not depend on that ambient\n     reset to size correctly in a fresh project that doesn't have\n     Tailwind — height/padding/border math below requires it directly. */\n  box-sizing: border-box;\n  position: relative;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  border: 1px solid transparent;\n  border-radius: var(--component-button-radius-control);\n  font-family: inherit;\n  font-weight: 500;\n  /* 1 clips descenders (y/g/j/p/q) under .label's overflow: hidden — the line\n     box was tighter than this font's real ascent+descent metrics. 1.2 is the\n     same ratio already verified safe for Tag/Badge's compact single-line\n     labels. */\n  line-height: 1.2;\n  white-space: nowrap;\n  cursor: pointer;\n  transition:\n    background-color 0.15s ease,\n    border-color 0.15s ease,\n    color 0.15s ease,\n    box-shadow 0.15s ease;\n}\n\n.visualSurface {\n  position: absolute;\n  inset: 0;\n  z-index: 0;\n  border: 1px solid transparent;\n  border-radius: inherit;\n  clip-path: var(--squircle-clip-path-control, none);\n  backdrop-filter: var(--glass-backdrop-filter-lg, none);\n  background-image: var(--component-surface-gradient-overlay);\n  pointer-events: none;\n}\n\n.content {\n  position: relative;\n  z-index: 1;\n  display: inline-flex;\n  min-width: 0;\n  align-items: center;\n  justify-content: center;\n  gap: var(--control-gap);\n}\n\n.button:focus {\n  outline: none;\n}\n\n.button:focus-visible {\n  outline: 2px solid var(--semantic-focus-ring);\n  /* CSS outline is painted outside the border edge and does not affect box\n     geometry. Zero offset is intentional: the canonical Figma contract has\n     no gap between Button and its focus ring. */\n  outline-offset: 0;\n}\n\n/* Joined item inside ButtonGroup — adjacency chrome lives on the group. */\n.inGroup {\n  position: relative;\n  border-radius: 0;\n  border-color: transparent;\n}\n\n.inGroup:focus-visible {\n  z-index: 1;\n}\n\n.inGroup .visualSurface {\n  /* Squircle clip fights shared outer chrome; flatten to inherited radius. */\n  clip-path: none;\n}\n\n.button:disabled,\n.button[aria-disabled=\"true\"] {\n  cursor: not-allowed;\n  pointer-events: none;\n  opacity: var(--opacity-disabled);\n}\n\n.primary:disabled,\n.primary[aria-disabled=\"true\"] {\n  color: var(--component-surface-content, var(--semantic-text-inverse));\n}\n\n.danger:disabled,\n.danger[aria-disabled=\"true\"] {\n  color: var(--component-surface-content, var(--semantic-text-inverse));\n}\n\n.primary:disabled .visualSurface,\n.primary[aria-disabled=\"true\"] .visualSurface {\n  background-color: var(--component-button-primary-fill);\n}\n\n.danger:disabled .visualSurface,\n.danger[aria-disabled=\"true\"] .visualSurface {\n  background-color: var(--component-danger-fill);\n}\n\n.fullWidth {\n  width: 100%;\n}\n\n.sizeSm {\n  min-height: var(--control-height-sm);\n  padding: 0 var(--control-padding-x-sm);\n  font-size: var(--control-font-size-sm);\n}\n\n.sizeMd {\n  min-height: var(--control-height-md);\n  padding: 0 var(--control-padding-x-md);\n  font-size: var(--control-font-size-md);\n}\n\n.sizeLg {\n  min-height: var(--control-height-lg);\n  padding: 0 var(--control-padding-x-lg);\n  font-size: var(--control-font-size-lg);\n}\n\n.primary {\n  /* Falls back to white outside Glass — see --component-surface-content in\n     styles/tokens.css. */\n  color: var(--component-surface-content, var(--semantic-text-inverse));\n}\n\n.primary .visualSurface {\n  background-color: var(--component-button-primary-fill);\n}\n\n.primary:hover:not(:disabled):not([aria-disabled=\"true\"]) .visualSurface {\n  background-color: var(--component-button-primary-fill-hover);\n}\n\n.primary:active:not(:disabled):not([aria-disabled=\"true\"]) .visualSurface {\n  background-color: var(--component-button-primary-fill-pressed);\n}\n\n.secondary {\n  color: var(--semantic-text-primary);\n}\n\n.secondary .visualSurface {\n  background-color: var(--surface-fill-control);\n  /* component/card/border-highlight-1 — Flat and Gradient resolve this\n     (and -2/-3) to the same solid value, so the first stop alone is the\n     correct single-color boundary here. See the Glass rim below for where\n     the three stops diverge into a real gradient. */\n  border-color: var(--component-card-border-highlight-1);\n}\n\n.secondary:hover:not(:disabled):not([aria-disabled=\"true\"]) .visualSurface {\n  background-color: var(--semantic-surface-subtle);\n  /* Hover is communicated by fill only — the boundary is unchanged from\n     Default, matching the verified Figma contract. Do not reintroduce a\n     border-color override here. */\n}\n\n.secondary:active:not(:disabled):not([aria-disabled=\"true\"]) .visualSurface {\n  background-color: var(--semantic-surface-elevated);\n}\n\n.danger {\n  /* component/button/danger/background — a dedicated, Figma-verified\n     Button-danger fill (#e5484d). Bound via --component-danger-fill,\n     separate from --semantic-action-danger (now also danger/500) used\n     elsewhere for Alert/Toast/focus rings. See styles/tokens.css. */\n  color: var(--component-surface-content, var(--semantic-text-inverse));\n}\n\n.danger .visualSurface {\n  background-color: var(--component-danger-fill);\n}\n\n.danger:hover:not(:disabled):not([aria-disabled=\"true\"]) .visualSurface {\n  background-color: var(--component-danger-fill-hover);\n}\n\n.danger:active:not(:disabled):not([aria-disabled=\"true\"]) .visualSurface {\n  background-color: var(--component-danger-fill-pressed);\n}\n\n/* component/button/primary/background and component/button/danger/\n   background — Glass only (2026-08-11). The explicit backdrop declarations\n   retain the same verified 16px component/surface/blur value already\n   applied by the shared Button rule. */\n[data-skrewww-surface=\"glass\"] .primary .visualSurface {\n  background-color: var(--component-brand-fill-glass);\n  backdrop-filter: var(--glass-backdrop-filter-lg);\n}\n\n[data-skrewww-surface=\"glass\"] .primary:hover:not(:disabled):not([aria-disabled=\"true\"]) .visualSurface {\n  background-color: var(--component-button-primary-fill-hover-glass);\n}\n\n[data-skrewww-surface=\"glass\"] .primary:active:not(:disabled):not([aria-disabled=\"true\"]) .visualSurface {\n  background-color: var(--component-button-primary-fill-pressed-glass);\n}\n\n[data-skrewww-surface=\"glass\"] .secondary .visualSurface {\n  background-color: var(--component-button-secondary-fill-glass);\n  backdrop-filter: var(--glass-backdrop-filter-lg);\n  /* The solid Flat/Gradient border-color is replaced by the ::before rim\n     below, exactly like Primary/Danger's Glass boundary — see the rim\n     comment further down. */\n  border-color: transparent;\n}\n\n[data-skrewww-surface=\"glass\"] .secondary:hover:not(:disabled):not([aria-disabled=\"true\"]) .visualSurface,\n[data-skrewww-surface=\"glass\"] .secondary:active:not(:disabled):not([aria-disabled=\"true\"]) .visualSurface {\n  background-color: var(--component-button-secondary-fill-elevated-glass);\n}\n\n[data-skrewww-surface=\"glass\"] .danger .visualSurface {\n  background-color: var(--component-danger-fill-glass);\n  backdrop-filter: var(--glass-backdrop-filter-lg);\n}\n\n[data-skrewww-surface=\"glass\"] .danger:hover:not(:disabled):not([aria-disabled=\"true\"]) .visualSurface {\n  background-color: var(--component-danger-fill-hover-glass);\n}\n\n[data-skrewww-surface=\"glass\"] .danger:active:not(:disabled):not([aria-disabled=\"true\"]) .visualSurface {\n  background-color: var(--component-danger-fill-pressed-glass);\n}\n\n/*\n * GLASS RIM GRADIENT — PRACTICAL APPROXIMATION, NOT EXACT PARITY.\n * Do not \"correct\" this back toward a naive angle; read this first.\n *\n * Figma stores this 1px stroke (strokeAlign INSIDE on all non-Focused\n * states) as a 3-stop gradient whose handles are normalized PER-AXIS to\n * the node's bounding box (x divided by width, y divided by height):\n *   start  (0, 0)\n *   end    (0.3022670192495956, 0.9571788442445038)\n *   width  (-0.4785894221222519, 0.1511335096247978)\n *\n * Because that normalization is non-uniform, the rendered angle depends on\n * the element's aspect ratio. Button is hug-content (width = padding +\n * label), so its width tracks the label text: the true Figma angle drifts\n * roughly 29 degrees between a 113px and a 400px Medium button. Matching\n * that exactly would need width-dependent runtime geometry, which is not\n * justified for a decorative 1px rim.\n *\n * We therefore ship one fixed angle, derived for the Medium master box\n * (113x36) and accepted as an approximation across all sizes:\n *   dx = 0.3022670192495956 * 113 = 34.156\n *   dy = 0.9571788442445038 * 36  = 34.458\n *   CSS uses 0deg = up, increasing clockwise, while Figma's Y axis points\n *   down, so angle = atan2(dx, -dy)\n *        = 180 - atan(34.156 / 34.458) = 180 - 44.748 = 135.25deg\n *\n * The practical CSS stop positions are 0% / 23.1% / 46.2%. The final colour\n * is repeated at 100% so it holds after 46.2% rather than stretching the\n * third transition across the rest of a variable-width Button.\n *\n * The mask/mask-composite technique below is what keeps this a true INSIDE\n * 1px border while preserving border-radius; border-image would not.\n */\n[data-skrewww-surface=\"glass\"] .primary .visualSurface::before,\n[data-skrewww-surface=\"glass\"] .danger .visualSurface::before,\n[data-skrewww-surface=\"glass\"] .secondary .visualSurface::before {\n  content: \"\";\n  position: absolute;\n  inset: 0;\n  pointer-events: none;\n  border: 1px solid transparent;\n  border-radius: inherit;\n  background-origin: border-box;\n  background-clip: border-box;\n  mask:\n    linear-gradient(#000 0 0) padding-box,\n    linear-gradient(#000 0 0);\n  mask-composite: exclude;\n}\n\n[data-skrewww-surface=\"glass\"] .primary .visualSurface::before {\n  background-image: linear-gradient(\n    135.25deg,\n    var(--component-button-primary-border-start) 0%,\n    var(--component-button-primary-border-mid) 23.1%,\n    var(--component-button-primary-border-end) 46.2%,\n    var(--component-button-primary-border-end) 100%\n  );\n}\n\n[data-skrewww-surface=\"glass\"] .danger .visualSurface::before {\n  background-image: linear-gradient(\n    135.25deg,\n    var(--component-button-danger-border-start) 0%,\n    var(--component-button-danger-border-mid) 23.1%,\n    var(--component-button-danger-border-end) 46.2%,\n    var(--component-button-danger-border-end) 100%\n  );\n}\n\n/* Secondary's Glass rim reuses component/card/border-highlight-1/2/3 (the\n   same token family verified for Card/Dialog) rather than a Button-specific\n   border token, since Secondary's boundary is a Card-family contract, not a\n   Primary/Danger-style brand-colored one. It reuses the 135.25deg angle\n   derived above rather than Dialog's 162.47deg: that angle is Card/Dialog's\n   own wide-panel aspect ratio, while Secondary shares Primary/Danger's exact\n   Medium master box geometry, so the angle already derived for this element\n   shape is the correct one to reuse here, not Dialog's. */\n[data-skrewww-surface=\"glass\"] .secondary .visualSurface::before {\n  background-image: linear-gradient(\n    135.25deg,\n    var(--component-card-border-highlight-1) 0%,\n    var(--component-card-border-highlight-2) 23.1%,\n    var(--component-card-border-highlight-3) 46.2%,\n    var(--component-card-border-highlight-3) 100%\n  );\n}\n\n@supports not (backdrop-filter: blur(1px)) {\n  [data-skrewww-surface=\"glass\"] .primary .visualSurface {\n    background-color: var(--component-button-primary-fill);\n  }\n\n  [data-skrewww-surface=\"glass\"] .secondary .visualSurface {\n    background-color: var(--semantic-surface-default);\n  }\n\n  [data-skrewww-surface=\"glass\"] .danger .visualSurface {\n    background-color: var(--component-danger-fill);\n  }\n\n  [data-skrewww-surface=\"glass\"] .primary:hover:not(:disabled):not([aria-disabled=\"true\"]) .visualSurface {\n    background-color: var(--component-button-primary-fill-hover);\n  }\n\n  [data-skrewww-surface=\"glass\"] .primary:active:not(:disabled):not([aria-disabled=\"true\"]) .visualSurface {\n    background-color: var(--component-button-primary-fill-pressed);\n  }\n\n  [data-skrewww-surface=\"glass\"] .secondary:hover:not(:disabled):not([aria-disabled=\"true\"]) .visualSurface,\n  [data-skrewww-surface=\"glass\"] .secondary:active:not(:disabled):not([aria-disabled=\"true\"]) .visualSurface {\n    background-color: var(--semantic-surface-elevated);\n  }\n\n  [data-skrewww-surface=\"glass\"] .danger:hover:not(:disabled):not([aria-disabled=\"true\"]) .visualSurface {\n    background-color: var(--component-danger-fill-hover);\n  }\n\n  [data-skrewww-surface=\"glass\"] .danger:active:not(:disabled):not([aria-disabled=\"true\"]) .visualSurface {\n    background-color: var(--component-danger-fill-pressed);\n  }\n}\n\n.icon {\n  display: inline-flex;\n  flex-shrink: 0;\n}\n\n.spinner {\n  animation: skrewww-spin 0.8s linear infinite;\n}\n\n.label {\n  display: inline-flex;\n  align-items: center;\n  min-width: 0;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .button {\n    transition: none;\n  }\n\n  .spinner {\n    animation: none;\n  }\n}\n\n@keyframes skrewww-spin {\n  to {\n    transform: rotate(360deg);\n  }\n}\n\n/*\n * Joined-item boundary suppression — placed last and keyed off\n * `.button.inGroup` (not bare `.inGroup`) so it outranks every\n * variant/Glass rule above regardless of cascade position. Secondary's own\n * border-color (`.secondary .visualSurface`) and the Glass rim's `::before`\n * gradient are each drawn per-button; inside a ButtonGroup/SplitButton they\n * would compound with the shared group chrome (wrapper border + 2px\n * divider-gap fill) into a doubled seam. Adjacency chrome lives on the\n * group only — see button-group.module.css.\n */\n.button.inGroup .visualSurface {\n  border-color: transparent;\n}\n\n.button.inGroup .visualSurface::before {\n  content: none;\n}\n",
      "type": "registry:ui",
      "target": "~/components/ui/button.module.css"
    },
    {
      "path": "lib/cn.ts",
      "content": "export function cn(...classes: Array<string | false | null | undefined>): string {\n  return classes.filter(Boolean).join(\" \");\n}\n",
      "type": "registry:lib",
      "target": "~/lib/cn.ts"
    },
    {
      "path": "components/ui/icons.tsx",
      "content": "export function LoadingSpinner({ className }: { className?: string }) {\n  return (\n    <svg\n      className={className}\n      width=\"16\"\n      height=\"16\"\n      viewBox=\"0 0 16 16\"\n      fill=\"none\"\n      aria-hidden=\"true\"\n    >\n      <circle\n        cx=\"8\"\n        cy=\"8\"\n        r=\"6\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2\"\n        strokeLinecap=\"round\"\n        strokeDasharray=\"28\"\n        strokeDashoffset=\"8\"\n      />\n    </svg>\n  );\n}\n\nexport function PlusIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      className={className}\n      width=\"16\"\n      height=\"16\"\n      viewBox=\"0 0 16 16\"\n      fill=\"none\"\n      aria-hidden=\"true\"\n    >\n      <path\n        d=\"M8 3.5v9M3.5 8h9\"\n        stroke=\"currentColor\"\n        strokeWidth=\"1.5\"\n        strokeLinecap=\"round\"\n      />\n    </svg>\n  );\n}\n\nexport function ChevronRightIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      className={className}\n      width=\"16\"\n      height=\"16\"\n      viewBox=\"0 0 16 16\"\n      fill=\"none\"\n      aria-hidden=\"true\"\n    >\n      <path\n        d=\"M6 4l4 4-4 4\"\n        stroke=\"currentColor\"\n        strokeWidth=\"1.5\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n}\n\nexport function SearchIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      className={className}\n      width=\"16\"\n      height=\"16\"\n      viewBox=\"0 0 16 16\"\n      fill=\"none\"\n      aria-hidden=\"true\"\n    >\n      <circle cx=\"7\" cy=\"7\" r=\"4.25\" stroke=\"currentColor\" strokeWidth=\"1.5\" />\n      <path\n        d=\"M10.5 10.5L13 13\"\n        stroke=\"currentColor\"\n        strokeWidth=\"1.5\"\n        strokeLinecap=\"round\"\n      />\n    </svg>\n  );\n}\n",
      "type": "registry:ui",
      "target": "~/components/ui/icons.tsx"
    },
    {
      "path": "components/ui/button-group-context.ts",
      "content": "\"use client\";\n\nimport { createContext, useContext } from \"react\";\n\n/**\n * When true, Button applies joined-item geometry for ButtonGroup /\n * SplitButton: zero individual radius (except first/last via group CSS),\n * transparent border, and no squircle clip that would fight the shared\n * outer chrome.\n */\nexport const ButtonGroupContext = createContext(false);\n\nexport function useButtonGroupItem(): boolean {\n  return useContext(ButtonGroupContext);\n}\n",
      "type": "registry:lib",
      "target": "~/components/ui/button-group-context.ts"
    },
    {
      "path": "components/ui/internal/link-utils.ts",
      "content": "/**\n * Framework-neutral link classification helpers.\n * No docs-site / repository configuration imports — origin is either passed\n * explicitly or read from the runtime location when available.\n */\n\nfunction normalizeHref(href: string): string {\n  return href.trim();\n}\n\nfunction resolveDefaultOrigin(): string | undefined {\n  if (typeof globalThis === \"undefined\") return undefined;\n  const locationLike = (globalThis as { location?: { origin?: string } }).location;\n  return locationLike?.origin;\n}\n\nexport function isHashHref(href: string): boolean {\n  return normalizeHref(href).startsWith(\"#\");\n}\n\nexport function isSpecialProtocolHref(href: string): boolean {\n  const value = normalizeHref(href).toLowerCase();\n  return value.startsWith(\"mailto:\") || value.startsWith(\"tel:\");\n}\n\nexport function isExternalHref(href: string, origin: string | undefined = resolveDefaultOrigin()): boolean {\n  const value = normalizeHref(href);\n\n  if (!value || isHashHref(value) || value.startsWith(\"/\") || value.startsWith(\"./\") || value.startsWith(\"../\")) {\n    return false;\n  }\n\n  if (isSpecialProtocolHref(value)) {\n    return true;\n  }\n\n  if (value.startsWith(\"//\")) {\n    return true;\n  }\n\n  if (/^https?:\\/\\//i.test(value)) {\n    if (!origin) {\n      // No origin context (SSR without an explicit origin) — treat absolute\n      // http(s) as external so NextLink is not used for foreign hosts.\n      return true;\n    }\n    try {\n      return new URL(value).origin !== origin;\n    } catch {\n      return true;\n    }\n  }\n\n  return false;\n}\n\nexport function shouldUseNativeAnchor(\n  href: string,\n  origin: string | undefined = resolveDefaultOrigin(),\n): boolean {\n  const value = normalizeHref(href);\n  return isSpecialProtocolHref(value) || isExternalHref(value, origin);\n}\n\nexport function getLinkRel(target?: string, rel?: string): string | undefined {\n  if (rel) return rel;\n  if (target === \"_blank\") return \"noopener noreferrer\";\n  return undefined;\n}\n\n/**\n * True only for root-relative internal paths (\"/components/button\"). These are\n * the only hrefs a host router can be asked to navigate to: hash-only links,\n * relative paths, protocol-relative URLs, mailto:/tel: and absolute URLs are\n * left to the browser so native behavior (scrolling, resolution against the\n * current URL, other origins) is never reimplemented.\n */\nexport function isRouterNavigableHref(href: string): boolean {\n  const value = normalizeHref(href);\n  return value.startsWith(\"/\") && !value.startsWith(\"//\");\n}\n\nexport type NavigationInterceptionInput = {\n  href: string;\n  target?: string;\n  download?: unknown;\n  button: number;\n  metaKey: boolean;\n  ctrlKey: boolean;\n  shiftKey: boolean;\n  altKey: boolean;\n  defaultPrevented: boolean;\n};\n\n/**\n * Whether a click on an anchor should be handed to the host router instead of\n * the browser. Only a plain, primary-button, same-window activation of an\n * internal path qualifies; every case where the browser should win returns\n * false.\n */\nexport function shouldInterceptNavigation(input: NavigationInterceptionInput): boolean {\n  if (input.defaultPrevented) return false;\n  if (input.button !== 0) return false;\n  if (input.metaKey || input.ctrlKey || input.shiftKey || input.altKey) return false;\n  if (input.target && input.target !== \"_self\") return false;\n  if (input.download !== undefined && input.download !== false) return false;\n  return isRouterNavigableHref(input.href);\n}\n",
      "type": "registry:lib",
      "target": "~/components/ui/internal/link-utils.ts"
    },
    {
      "path": "components/ui/router-navigation.tsx",
      "content": "\"use client\";\n\nimport {\n  createContext,\n  forwardRef,\n  useContext,\n  type AnchorHTMLAttributes,\n  type MouseEvent,\n  type ReactNode,\n} from \"react\";\nimport { shouldInterceptNavigation } from \"@/components/ui/internal/link-utils\";\n\n/**\n * Router integration for link-bearing components (Button, Link, Pagination,\n * List Item). Components always render a native `<a href>`; with no provider\n * a click is ordinary browser navigation. A host application may mount one\n * `SkrewwwRouterProvider` so eligible internal navigations are handed to its\n * own router instead of reloading the page. The contract is navigation\n * intent (\"go to this path\"), not any particular router.\n */\nexport type SkrewwwNavigate = (href: string) => void;\n\nconst SkrewwwNavigateContext = createContext<SkrewwwNavigate | null>(null);\n\nexport type SkrewwwRouterProviderProps = {\n  /**\n   * Called with the root-relative href of a plain, same-window click on an\n   * internal link. Modified or non-primary clicks, `target` other than\n   * `_self`, `download`, prevented events and every non-root-relative href\n   * are never passed to it. Memoize it to avoid re-rendering link consumers.\n   */\n  navigate: SkrewwwNavigate;\n  children: ReactNode;\n};\n\nexport function SkrewwwRouterProvider({ navigate, children }: SkrewwwRouterProviderProps) {\n  return <SkrewwwNavigateContext.Provider value={navigate}>{children}</SkrewwwNavigateContext.Provider>;\n}\n\nexport type RouterAnchorProps = AnchorHTMLAttributes<HTMLAnchorElement> & { href: string };\n\n/** Internal building block — not part of the public package API. */\nexport const RouterAnchor = forwardRef<HTMLAnchorElement, RouterAnchorProps>(function RouterAnchor(\n  { href, target, download, onClick, ...rest },\n  ref,\n) {\n  const navigate = useContext(SkrewwwNavigateContext);\n\n  function handleClick(event: MouseEvent<HTMLAnchorElement>) {\n    onClick?.(event);\n    if (!navigate) return;\n    const intercept = shouldInterceptNavigation({\n      href,\n      target,\n      download,\n      button: event.button,\n      metaKey: event.metaKey,\n      ctrlKey: event.ctrlKey,\n      shiftKey: event.shiftKey,\n      altKey: event.altKey,\n      defaultPrevented: event.defaultPrevented,\n    });\n    if (!intercept) return;\n    event.preventDefault();\n    navigate(href);\n  }\n\n  return <a ref={ref} href={href} target={target} download={download} onClick={handleClick} {...rest} />;\n});\n",
      "type": "registry:ui",
      "target": "~/components/ui/router-navigation.tsx"
    }
  ]
}
