{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"hyperlink","type":"registry:ui","title":"Hyperlink","description":"An inline link whose underline draws in from the left and exits to the right, with a springy arrow nudge and a hover or focus preview card for the destination.","dependencies":["motion","lucide-react"],"registryDependencies":[],"files":[{"path":"registry/ui/hyperlink.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useId, useRef, useState, type AnchorHTMLAttributes, type CSSProperties, type ReactNode } from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { ArrowUpRight } from \"lucide-react\";\n\nexport interface HyperlinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {\n  href: string;\n  children: ReactNode;\n  /** Headline of the hover preview card. No title and no description = no card. */\n  previewTitle?: string;\n  previewDescription?: string;\n  /** Underline and arrow color. */\n  color?: string;\n  /** Show the nudging arrow. Defaults to true for external links. */\n  arrow?: boolean;\n  /** Ms of hover before the preview card opens. */\n  delay?: number;\n}\n\nconst css = `.hyperlink{background:linear-gradient(var(--hl),var(--hl)) 100% 100%/0% 1.5px no-repeat,linear-gradient(color-mix(in oklab,var(--hl) 30%,transparent),color-mix(in oklab,var(--hl) 30%,transparent)) 0 100%/100% 1px no-repeat;-webkit-box-decoration-break:clone;box-decoration-break:clone;transition:background-size .4s cubic-bezier(.65,0,.35,1)}\n.hyperlink:hover,.hyperlink:focus-visible{background-position:0 100%,0 100%;background-size:100% 1.5px,100% 1px}\n.hyperlink-arrow{display:inline-block;transition:transform .35s cubic-bezier(.34,1.8,.64,1)}\n.hyperlink:hover .hyperlink-arrow,.hyperlink:focus-visible .hyperlink-arrow{transform:translate(2px,-2px)}\n@media (prefers-reduced-motion:reduce){.hyperlink,.hyperlink-arrow{transition:none}}`;\n\nconst hostOf = (href: string) => {\n  try {\n    return new URL(href).hostname.replace(/^www\\./, \"\");\n  } catch {\n    return href;\n  }\n};\n\n// Deterministic hue per domain, so every site gets its own thumbnail color.\nconst hueOf = (s: string) => [...s].reduce((h, c) => (h * 31 + c.charCodeAt(0)) % 360, 7);\n\nexport function Hyperlink({\n  href,\n  children,\n  previewTitle,\n  previewDescription,\n  color = \"currentColor\",\n  arrow,\n  delay = 250,\n  className,\n  style,\n  onFocus,\n  onBlur,\n  ...props\n}: HyperlinkProps) {\n  const id = useId();\n  const reduce = useReducedMotion();\n  const [open, setOpen] = useState(false);\n  const timer = useRef<number | undefined>(undefined);\n  const external = /^https?:\\/\\//.test(href);\n  const showArrow = arrow ?? external;\n  const hasCard = Boolean(previewTitle || previewDescription);\n  const host = hostOf(href);\n  const hue = hueOf(host);\n\n  const show = () => {\n    if (!hasCard) return;\n    window.clearTimeout(timer.current);\n    timer.current = window.setTimeout(() => setOpen(true), delay);\n  };\n  const hide = () => {\n    window.clearTimeout(timer.current);\n    timer.current = window.setTimeout(() => setOpen(false), 80);\n  };\n  useEffect(() => () => window.clearTimeout(timer.current), []);\n\n  return (\n    <span\n      className=\"relative\"\n      onPointerEnter={show}\n      onPointerLeave={hide}\n      onKeyDown={(e) => e.key === \"Escape\" && setOpen(false)}\n    >\n      <style href=\"hyperlink\" precedence=\"default\">\n        {css}\n      </style>\n      <a\n        href={href}\n        target={external ? \"_blank\" : undefined}\n        rel={external ? \"noopener noreferrer\" : undefined}\n        aria-describedby={open ? id : undefined}\n        {...props}\n        onFocus={(e) => {\n          show();\n          onFocus?.(e);\n        }}\n        onBlur={(e) => {\n          hide();\n          onBlur?.(e);\n        }}\n        className={`hyperlink rounded-sm pb-0.5 font-medium text-foreground no-underline outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring ${className ?? \"\"}`}\n        style={{ \"--hl\": color, ...style } as CSSProperties}\n      >\n        {children}\n        {showArrow && (\n          <span className=\"hyperlink-arrow ml-0.5 align-[-0.1em]\" style={{ color }}>\n            <ArrowUpRight aria-hidden className=\"size-[0.95em]\" />\n          </span>\n        )}\n        {external && <span className=\"sr-only\"> (opens in a new tab)</span>}\n      </a>\n      <AnimatePresence>\n        {open && (\n          <motion.span\n            id={id}\n            role=\"tooltip\"\n            className=\"absolute bottom-full left-0 z-50 block w-72 pb-2.5\"\n            style={{ transformOrigin: \"20% 100%\", transformPerspective: 600 }}\n            initial={reduce ? { opacity: 0 } : { opacity: 0, y: 10, scale: 0.92, rotateX: 18 }}\n            animate={{ opacity: 1, y: 0, scale: 1, rotateX: 0 }}\n            exit={reduce ? { opacity: 0 } : { opacity: 0, y: 6, scale: 0.96, transition: { duration: 0.12 } }}\n            transition={{ type: \"spring\", stiffness: 420, damping: 22 }}\n          >\n            <span className=\"block overflow-hidden rounded-xl border border-border bg-card text-left shadow-2xl shadow-black/30\">\n              <span\n                aria-hidden\n                className=\"block h-20\"\n                style={{\n                  background: `radial-gradient(120% 140% at 0% 0%, hsl(${hue} 90% 70%), transparent 60%), radial-gradient(120% 140% at 100% 100%, hsl(${(hue + 60) % 360} 85% 55%), hsl(${(hue + 200) % 360} 60% 25%))`,\n                }}\n              />\n              <span className=\"block p-3\">\n                <span className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n                  <span\n                    aria-hidden\n                    className=\"grid size-4 place-items-center rounded text-[10px] font-bold text-black uppercase\"\n                    style={{ background: `hsl(${hue} 90% 70%)` }}\n                  >\n                    {host[0]}\n                  </span>\n                  {host}\n                </span>\n                {previewTitle && <span className=\"mt-1.5 block text-sm font-semibold text-foreground\">{previewTitle}</span>}\n                {previewDescription && (\n                  <span className=\"mt-0.5 line-clamp-2 block text-xs text-muted-foreground\">{previewDescription}</span>\n                )}\n              </span>\n            </span>\n          </motion.span>\n        )}\n      </AnimatePresence>\n    </span>\n  );\n}\n"}]}