{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "link",
  "title": "Link",
  "description": "Link from the Get IT Done design system.",
  "dependencies": [
    "@radix-ui/react-slot@^1.5.0",
    "class-variance-authority@^0.7.0"
  ],
  "registryDependencies": [
    "https://zero-design.itdone.de/r/recipes.json",
    "https://zero-design.itdone.de/r/utils.json",
    "https://zero-design.itdone.de/r/theme.json"
  ],
  "files": [
    {
      "path": "registry/ui/link.tsx",
      "content": "'use client';\n\nimport { Slot } from '@radix-ui/react-slot';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport * as React from 'react';\n\nimport { focusRing } from '@/lib/recipes';\nimport { cn } from '@/lib/utils';\n\n// Figma Link (7747:83048): Size sm|md|lg|xl|2xl, Color Brand|Neutral|Destructive, Icon Default|Dot leading,\n// State Default|Hover|Active|Focused|Disabled. All sizes use the link-sm type role in Figma; 2xl widens the gap and icons.\nconst linkVariants = cva(\n  [\n    'inline-flex items-center justify-center rounded-item underline-offset-4 transition-colors',\n    'active:underline',\n    // A disabled link keeps pointer events so a tooltip can explain it. The click guard in the component blocks the action.\n    'aria-disabled:cursor-not-allowed aria-disabled:text-text-disabled aria-disabled:hover:text-text-disabled aria-disabled:active:text-text-disabled aria-disabled:hover:no-underline aria-disabled:active:no-underline',\n    ...focusRing,\n  ],\n  {\n    variants: {\n      color: {\n        brand: 'text-link-default hover:text-link-hover active:text-link-active',\n        neutral: 'text-text-subtle hover:text-text-default active:text-text-default',\n        destructive: 'text-text-destructive hover:text-general-error-hover active:text-general-error-active',\n        // For inverted and dark surfaces (pairs with bg-background-inverse).\n        inverse: 'text-text-inverse hover:underline',\n        // Takes the colour of the surrounding text, for example inside an Alert. Underlined so it is not colour only.\n        inherit: 'text-inherit underline hover:no-underline',\n      },\n      // link sets the link-sm type role. inherit takes font family, size, weight and line height from the surrounding text (inline links in body copy, headings, buttons).\n      typography: {\n        link: 'typography-link-sm',\n        // 16px link role (typography-link-md), for body copy at 16px.\n        'link-md': 'typography-link-md',\n        inherit: '[font:inherit]',\n      },\n      // Underline control. Unset keeps the colour default (brand and neutral: underline on press, inherit: underlined until hover).\n      underline: {\n        always: 'underline hover:underline active:underline',\n        hover: 'no-underline hover:underline active:underline',\n        none: 'no-underline hover:no-underline active:no-underline',\n      },\n      // Font weight. Unset keeps the weight of the typography role (link role) or the surrounding text (inherit).\n      weight: {\n        regular: 'font-normal',\n        medium: 'font-medium',\n        semibold: 'font-semibold',\n      },\n      // Letter spacing. Unset keeps the typography role. normal resets it, inherit takes it from the surrounding text.\n      tracking: {\n        normal: 'tracking-normal',\n        inherit: '[letter-spacing:inherit]',\n      },\n      // Line height. Unset keeps the one of the typography role (20px for the link roles). inherit takes it from the surrounding text, so a 16px link fits a 24px body row.\n      leading: {\n        inherit: '[line-height:inherit]',\n      },\n      size: {\n        sm: 'gap-2',\n        md: 'gap-2',\n        lg: 'gap-2',\n        xl: 'gap-2',\n        '2xl': 'gap-3',\n      },\n    },\n    defaultVariants: { color: 'brand', size: 'md', typography: 'link' },\n  },\n);\n\nexport interface LinkProps\n  extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'color'>,\n    VariantProps<typeof linkVariants> {\n  // Renders the single child (e.g. a router link) with the link styling instead of an `<a>`.\n  asChild?: boolean;\n  // Leading icon (Figma Icon=Default, Icon leading). Ignored when `dotLeading` is set.\n  icon?: React.ReactNode;\n  // Trailing icon (Figma Icon trailing).\n  iconTrailing?: React.ReactNode;\n  // Leading status dot instead of an icon (Figma Icon=Dot leading).\n  dotLeading?: boolean;\n  // Dims the link and removes it from the tab order (Figma State=Disabled).\n  disabled?: boolean;\n  // Called instead of `onClick` while the link is disabled or has `aria-disabled`. Explain why here. Nothing else happens. A link with only `aria-disabled` stays in the tab order, so the explanation is reachable by keyboard.\n  onGatedClick?: (event: React.MouseEvent<HTMLAnchorElement>) => void;\n}\n\nconst Link = React.forwardRef<HTMLAnchorElement, LinkProps>(\n  (\n    { className, color, size, typography, underline, weight, tracking, leading, asChild = false, icon, iconTrailing, dotLeading = false, disabled = false, children, onClick, onGatedClick, href, ...props },\n    ref,\n  ) => {\n    const iconClass = cn('inline-flex shrink-0 items-center justify-center', size === '2xl' ? 'h-icon-lg w-icon-lg' : 'h-icon-md w-icon-md');\n    const withSlots = (content: React.ReactNode) => (\n      <>\n        {dotLeading ? (\n          <span aria-hidden=\"true\" className=\"h-2 w-2 shrink-0 rounded-full bg-current\" />\n        ) : (\n          icon && (\n            <span aria-hidden=\"true\" className={iconClass}>\n              {icon}\n            </span>\n          )\n        )}\n        {content}\n        {iconTrailing && (\n          <span aria-hidden=\"true\" className={iconClass}>\n            {iconTrailing}\n          </span>\n        )}\n      </>\n    );\n    // With asChild the slots go inside the single child element, so the child stays the one focusable link.\n    const child = asChild && React.isValidElement(children) ? (children as React.ReactElement<{ children?: React.ReactNode }>) : null;\n    const Comp = asChild ? Slot : 'a';\n    // disabled leaves the tab order and drops the href. aria-disabled alone is gated: it stays focusable so a keyboard user reaches the explanation.\n    const gated = !disabled && (props['aria-disabled'] === true || props['aria-disabled'] === 'true');\n    const blocked = disabled || gated;\n    return (\n      <Comp\n        ref={ref}\n        href={disabled ? undefined : href}\n        aria-disabled={blocked || undefined}\n        tabIndex={disabled ? -1 : props.tabIndex}\n        onClick={\n          blocked\n            ? (e: React.MouseEvent<HTMLAnchorElement>) => {\n                e.preventDefault();\n                e.stopPropagation();\n                onGatedClick?.(e);\n              }\n            : onClick\n        }\n        className={cn(linkVariants({ color, size, typography, underline, weight, tracking, leading }), className)}\n        {...props}\n      >\n        {child ? React.cloneElement(child, undefined, withSlots(child.props.children)) : withSlots(children)}\n      </Comp>\n    );\n  },\n);\nLink.displayName = 'Link';\n\nexport { Link, linkVariants };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}