{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "link-preview",
  "title": "Link preview",
  "description": "A text trigger reveals a screenshot of the destination webpage on hover, keyboard focus or tap. The screenshot fills the card; its address rises in on a frosted chip once the pointer is on it.",
  "dependencies": [
    "radix-ui",
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/items/link-preview/link-preview.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState, type ReactElement, type ReactNode } from \"react\";\nimport { Popover } from \"radix-ui\";\nimport { ArrowUpRight, X } from \"lucide-react\";\nimport { motion, useReducedMotionConfig } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type LinkPreviewProps = {\n  children?: ReactNode;\n  /** Use an existing link or logo button as the trigger. */\n  trigger?: ReactElement;\n  /** Keep the preview open and explain when its destination is the current site. */\n  isCurrentSite?: boolean;\n  href: string;\n  title: string;\n  /** A screenshot of the destination page. */\n  imageSrc: string;\n  side?: \"top\" | \"right\" | \"bottom\" | \"left\";\n};\n\nfunction hostnameOf(href?: string) {\n  if (!href?.startsWith(\"http\")) return undefined;\n  try {\n    return new URL(href).hostname.replace(/^www\\./, \"\");\n  } catch {\n    return undefined;\n  }\n}\n\n/** A page screenshot on hover, focus or tap. No metadata or screenshot service required. */\nexport function LinkPreview({ children, trigger, isCurrentSite = false, href, title, imageSrc, side = \"top\" }: LinkPreviewProps) {\n  const [open, setOpen] = useState(false);\n  const [showNotice, setShowNotice] = useState(false);\n  const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n  const reduce = useReducedMotionConfig();\n  const dismissed = useRef(false);\n  const safeHref = /^(https?:\\/\\/|\\/(?!\\/))/.test(href) ? href : undefined;\n  const address = hostnameOf(safeHref) ?? title;\n\n  function cancelClose() {\n    if (closeTimer.current) clearTimeout(closeTimer.current);\n  }\n  function show() { cancelClose(); dismissed.current = false; setOpen(true); }\n  function hide() { dismissed.current = true; setOpen(false); setShowNotice(false); }\n  function scheduleClose() {\n    cancelClose();\n    closeTimer.current = setTimeout(hide, 200);\n  }\n  useEffect(() => () => { if (closeTimer.current) clearTimeout(closeTimer.current); }, []);\n\n  // A portable registry component: consumers supply a page screenshot.\n  const screenshot = (\n    // eslint-disable-next-line @next/next/no-img-element\n    <img src={imageSrc} alt={`Screenshot of ${title}`} loading=\"lazy\" className=\"block h-auto min-h-40 w-full object-contain object-top\" />\n  );\n\n  return (\n    <Popover.Root open={open} onOpenChange={(next) => { cancelClose(); if (next) show(); else hide(); }}>\n      <Popover.Trigger\n        {...{ asChild: true }}\n        onPointerEnter={(event) => { if (event.pointerType === \"mouse\") show(); }}\n        onPointerLeave={(event) => { if (event.pointerType === \"mouse\") scheduleClose(); }}\n        onFocus={(event) => { if (!dismissed.current && event.currentTarget.matches(\":focus-visible\")) show(); }}\n        onBlur={() => { dismissed.current = false; }}\n        onClick={(event) => {\n          if (isCurrentSite) {\n            event.preventDefault();\n            cancelClose();\n            setShowNotice(true);\n            setOpen(true);\n          }\n        }}\n      >\n        {trigger ?? (\n          <button type=\"button\" className=\"inline-flex min-h-11 items-center gap-1 font-body text-[14px] font-medium text-lead underline decoration-rule underline-offset-4 transition-colors hover:decoration-lead focus-visible:rounded-inset focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ring\">\n            {children}<ArrowUpRight aria-hidden=\"true\" className=\"size-3.5\" />\n          </button>\n        )}\n      </Popover.Trigger>\n      <Popover.Portal>\n        <Popover.Content\n          {...{ asChild: true }} side={side} sideOffset={10} collisionPadding={16}\n          onOpenAutoFocus={(event) => event.preventDefault()}\n          onPointerEnter={cancelClose}\n          onPointerLeave={(event) => { if (event.pointerType === \"mouse\" && !event.currentTarget.contains(document.activeElement)) scheduleClose(); }}\n          aria-label={title}\n        >\n          <motion.div\n            initial={{ opacity: 0, scale: reduce ? 1 : 0.96, y: reduce ? 0 : 4 }}\n            animate={{ opacity: 1, scale: 1, y: 0 }}\n            transition={reduce ? { duration: 0 } : { type: \"spring\", duration: 0.32, bounce: 0.12 }}\n            className=\"group/preview z-50 w-[400px] max-w-[calc(100vw-2rem)] origin-(--radix-popover-content-transform-origin) rounded-panel border border-rule bg-popover p-1 font-body text-popover-foreground shadow-xl\"\n          >\n            {/* The screenshot is the card: the frame's padding makes panel radius minus padding land on the inset radius. */}\n            <div className=\"relative overflow-hidden rounded-inset\">\n              <div className=\"max-h-80 overflow-y-auto overscroll-contain\" tabIndex={0} role=\"region\" aria-label={`${title} page screenshot`}>\n                {isCurrentSite ? (\n                  <button type=\"button\" onClick={() => setShowNotice(true)} aria-label={`About ${title}`} className=\"block w-full cursor-pointer focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring\">{screenshot}</button>\n                ) : safeHref ? (\n                  <a href={safeHref} target=\"_blank\" rel=\"noopener noreferrer\" aria-label={`Visit ${title} (opens in a new tab)`} className=\"block cursor-pointer focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring\">{screenshot}</a>\n                ) : screenshot}\n              </div>\n              <span aria-hidden=\"true\" className=\"pointer-events-none absolute inset-0 rounded-inset ring-1 ring-rule ring-inset\" />\n              {/* At rest the card is only the page; the address rises in once the pointer or focus is on it. */}\n              <span\n                aria-hidden={isCurrentSite ? undefined : true}\n                className={cn(\n                  \"pointer-events-none absolute bottom-2 left-2 inline-flex max-w-[calc(100%-1rem)] items-center gap-1 rounded-full bg-popover/85 py-1 pr-2 pl-2.5 text-[11px] font-medium text-ink shadow-sm ring-1 ring-rule backdrop-blur-md transition duration-200 motion-reduce:transition-none\",\n                  showNotice\n                    ? \"translate-y-0 opacity-100\"\n                    : \"translate-y-1 opacity-0 group-focus-within/preview:translate-y-0 group-focus-within/preview:opacity-100 group-hover/preview:translate-y-0 group-hover/preview:opacity-100 [@media(hover:none)]:translate-y-0 [@media(hover:none)]:opacity-100\",\n                )}\n              >\n                {isCurrentSite ? (\n                  <span role=\"status\" className=\"truncate\">{showNotice ? \"You’re already on the bash-ui website.\" : address}</span>\n                ) : (\n                  <><span className=\"truncate\">{address}</span>{safeHref && <ArrowUpRight className=\"size-3 shrink-0 text-dim\" />}</>\n                )}\n              </span>\n              {/* A mouse closes the card by leaving it, so the button only shows for touch and keyboard. */}\n              <Popover.Close aria-label=\"Close preview\" className=\"absolute top-2 right-2 grid size-7 place-items-center rounded-full bg-popover/85 text-ink opacity-0 shadow-sm ring-1 ring-rule backdrop-blur-md transition-opacity before:absolute before:-inset-2 before:content-[''] focus-visible:opacity-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring motion-reduce:transition-none [@media(hover:none)]:opacity-100\"><X className=\"size-3.5\" /></Popover.Close>\n            </div>\n          </motion.div>\n        </Popover.Content>\n      </Popover.Portal>\n    </Popover.Root>\n  );\n}\n",
      "type": "registry:component"
    }
  ],
  "meta": {
    "kind": "widget",
    "whenToUse": "Use for portfolios, references and policy summaries where readers benefit from context before navigating. Supply a screenshot of the page; this component does not capture websites or fetch metadata. Keep essential information available on the destination page.",
    "origin": {
      "project": "bash-ui",
      "path": "registry/items/link-preview/link-preview.tsx"
    },
    "credits": []
  },
  "type": "registry:component"
}
