{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "link-preview",
  "type": "registry:ui",
  "title": "Link Preview",
  "description": "An interactive link component with rich preview tooltip showing website metadata like title, description, and images.",
  "registryDependencies": [
    "icons"
  ],
  "files": [
    {
      "path": "registry/new-york/ui/link-preview.tsx",
      "content": "\"use client\";\n\nimport Image from \"next/image\";\nimport { type ReactNode, useEffect, useRef, useState } from \"react\";\nimport { Globe02Icon, HugeiconsIcon } from \"@/components/icons\";\n\nimport {\n\tTooltip,\n\tTooltipContent,\n\tTooltipTrigger,\n} from \"@/components/ui/tooltip\";\n\ninterface UrlMetadata {\n\ttitle: string | null;\n\tdescription: string | null;\n\tfavicon: string | null;\n\twebsite_name: string | null;\n\twebsite_image: string | null;\n\turl: string;\n}\n\ninterface LinkPreviewProps {\n\thref: string;\n\tchildren: ReactNode | string | null;\n\tclassName?: string;\n}\n\nconst isEmail = (str: string) => /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(str);\n\nconst isValidHttpUrl = (str: string): boolean => {\n\ttry {\n\t\tconst url = new URL(str);\n\t\treturn /^(http|https):$/.test(url.protocol);\n\t} catch {\n\t\treturn false;\n\t}\n};\n\nexport function LinkPreview({\n\thref,\n\tchildren,\n\tclassName = \"cursor-pointer rounded-sm bg-primary/20 px-1 text-sm font-medium text-primary transition-all hover:text-white hover:underline\",\n}: LinkPreviewProps) {\n\tconst elementRef = useRef<HTMLAnchorElement>(null);\n\tconst [isInView, setIsInView] = useState(false);\n\tconst [validFavicon, setValidFavicon] = useState(true);\n\tconst [validImage, setValidImage] = useState(true);\n\tconst [metadata, setMetadata] = useState<UrlMetadata | null>(null);\n\tconst [isLoading, setIsLoading] = useState(false);\n\tconst [error, setError] = useState<Error | null>(null);\n\n\tconst isValidUrl =\n\t\thref &&\n\t\tisValidHttpUrl(href) &&\n\t\t!isEmail(href) &&\n\t\t!href.startsWith(\"mailto:\");\n\n\t// Fetch metadata when in view\n\tuseEffect(() => {\n\t\tif (!isInView || !isValidUrl || metadata) return;\n\n\t\tlet isMounted = true;\n\n\t\tasync function fetchMetadata() {\n\t\t\tsetIsLoading(true);\n\t\t\tsetError(null);\n\n\t\t\ttry {\n\t\t\t\t// Fetch the URL directly\n\t\t\t\tconst response = await fetch(href);\n\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\tthrow new Error(\"Failed to fetch URL\");\n\t\t\t\t}\n\n\t\t\t\tconst html = await response.text();\n\n\t\t\t\t// Extract metadata from HTML\n\t\t\t\tconst getMetaTag = (name: string): string | null => {\n\t\t\t\t\tconst patterns = [\n\t\t\t\t\t\tnew RegExp(\n\t\t\t\t\t\t\t`<meta[^>]*property=[\"']${name}[\"'][^>]*content=[\"']([^\"']*)[\"']`,\n\t\t\t\t\t\t\t\"i\",\n\t\t\t\t\t\t),\n\t\t\t\t\t\tnew RegExp(\n\t\t\t\t\t\t\t`<meta[^>]*name=[\"']${name}[\"'][^>]*content=[\"']([^\"']*)[\"']`,\n\t\t\t\t\t\t\t\"i\",\n\t\t\t\t\t\t),\n\t\t\t\t\t\tnew RegExp(\n\t\t\t\t\t\t\t`<meta[^>]*content=[\"']([^\"']*)[\"'][^>]*property=[\"']${name}[\"']`,\n\t\t\t\t\t\t\t\"i\",\n\t\t\t\t\t\t),\n\t\t\t\t\t\tnew RegExp(\n\t\t\t\t\t\t\t`<meta[^>]*content=[\"']([^\"']*)[\"'][^>]*name=[\"']${name}[\"']`,\n\t\t\t\t\t\t\t\"i\",\n\t\t\t\t\t\t),\n\t\t\t\t\t];\n\n\t\t\t\t\tfor (const pattern of patterns) {\n\t\t\t\t\t\tconst match = html.match(pattern);\n\t\t\t\t\t\tif (match) return match[1];\n\t\t\t\t\t}\n\t\t\t\t\treturn null;\n\t\t\t\t};\n\n\t\t\t\tconst titleMatch = html.match(/<title[^>]*>([^<]+)<\\/title>/i);\n\t\t\t\tconst urlObj = new URL(href);\n\n\t\t\t\tconst data: UrlMetadata = {\n\t\t\t\t\ttitle:\n\t\t\t\t\t\tgetMetaTag(\"og:title\") ||\n\t\t\t\t\t\tgetMetaTag(\"twitter:title\") ||\n\t\t\t\t\t\ttitleMatch?.[1] ||\n\t\t\t\t\t\tnull,\n\t\t\t\t\tdescription:\n\t\t\t\t\t\tgetMetaTag(\"og:description\") ||\n\t\t\t\t\t\tgetMetaTag(\"twitter:description\") ||\n\t\t\t\t\t\tgetMetaTag(\"description\") ||\n\t\t\t\t\t\tnull,\n\t\t\t\t\twebsite_image:\n\t\t\t\t\t\tgetMetaTag(\"og:image\") || getMetaTag(\"twitter:image\") || null,\n\t\t\t\t\tfavicon:\n\t\t\t\t\t\tgetMetaTag(\"icon\") ||\n\t\t\t\t\t\tgetMetaTag(\"shortcut icon\") ||\n\t\t\t\t\t\t`${urlObj.origin}/favicon.ico`,\n\t\t\t\t\twebsite_name: getMetaTag(\"og:site_name\") || urlObj.hostname,\n\t\t\t\t\turl: href,\n\t\t\t\t};\n\n\t\t\t\tif (isMounted) {\n\t\t\t\t\tsetMetadata(data);\n\t\t\t\t\tsetIsLoading(false);\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\tif (isMounted) {\n\t\t\t\t\tsetError(err as Error);\n\t\t\t\t\tsetIsLoading(false);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfetchMetadata();\n\n\t\treturn () => {\n\t\t\tisMounted = false;\n\t\t};\n\t}, [isInView, isValidUrl, href, metadata]);\n\n\t// Set up intersection observer to detect when element is in view\n\tuseEffect(() => {\n\t\tconst element = elementRef.current;\n\t\tif (!element || !href) return;\n\n\t\tconst observer = new IntersectionObserver(\n\t\t\t([entry]) => {\n\t\t\t\tif (entry.isIntersecting) {\n\t\t\t\t\tsetIsInView(true);\n\t\t\t\t\tobserver.unobserve(element);\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\trootMargin: \"100px\", // Start fetching 100px before element comes into view\n\t\t\t\tthreshold: 0.1,\n\t\t\t},\n\t\t);\n\n\t\tobserver.observe(element);\n\n\t\treturn () => {\n\t\t\tobserver.unobserve(element);\n\t\t};\n\t}, [href]);\n\n\tif (!href) return null;\n\n\treturn (\n\t\t<Tooltip>\n\t\t\t<TooltipTrigger asChild>\n\t\t\t\t<a\n\t\t\t\t\tref={elementRef}\n\t\t\t\t\thref={href}\n\t\t\t\t\tclassName={className}\n\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t>\n\t\t\t\t\t{children}\n\t\t\t\t</a>\n\t\t\t</TooltipTrigger>\n\t\t\t<TooltipContent className=\"max-w-[280px] border border-zinc-700 bg-zinc-900 p-3 text-white shadow-lg\">\n\t\t\t\t{isLoading ? (\n\t\t\t\t\t<div className=\"flex justify-center p-5\">\n\t\t\t\t\t\t<div className=\"size-5 animate-spin rounded-full border-2 border-zinc-700 border-t-white\" />\n\t\t\t\t\t</div>\n\t\t\t\t) : error || !isValidUrl ? (\n\t\t\t\t\t<div className=\"flex items-center gap-2 p-3 text-red-400\">\n\t\t\t\t\t\t<HugeiconsIcon icon={Globe02Icon} size={16} />\n\t\t\t\t\t\t<span className=\"text-sm\">\n\t\t\t\t\t\t\t{!isValidUrl ? \"Invalid URL\" : \"Failed to load preview\"}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t) : metadata ? (\n\t\t\t\t\t<div className=\"flex w-full flex-col gap-2\">\n\t\t\t\t\t\t{/* Website Image */}\n\t\t\t\t\t\t{metadata.website_image && validImage && (\n\t\t\t\t\t\t\t<div className=\"relative aspect-video w-full overflow-hidden rounded-lg\">\n\t\t\t\t\t\t\t\t<Image\n\t\t\t\t\t\t\t\t\tsrc={metadata.website_image}\n\t\t\t\t\t\t\t\t\talt=\"Website preview\"\n\t\t\t\t\t\t\t\t\tfill\n\t\t\t\t\t\t\t\t\tclassName=\"rounded-lg object-cover\"\n\t\t\t\t\t\t\t\t\tonError={() => setValidImage(false)}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)}\n\n\t\t\t\t\t\t{/* Website Name & Favicon */}\n\t\t\t\t\t\t{(metadata.website_name || (metadata.favicon && validFavicon)) && (\n\t\t\t\t\t\t\t<div className=\"flex items-center gap-2\">\n\t\t\t\t\t\t\t\t{metadata.favicon && validFavicon ? (\n\t\t\t\t\t\t\t\t\t<Image\n\t\t\t\t\t\t\t\t\t\twidth={20}\n\t\t\t\t\t\t\t\t\t\theight={20}\n\t\t\t\t\t\t\t\t\t\talt=\"Favicon\"\n\t\t\t\t\t\t\t\t\t\tclassName=\"size-5 rounded-full\"\n\t\t\t\t\t\t\t\t\t\tsrc={metadata.favicon}\n\t\t\t\t\t\t\t\t\t\tonError={() => setValidFavicon(false)}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t<HugeiconsIcon\n\t\t\t\t\t\t\t\t\t\ticon={Globe02Icon}\n\t\t\t\t\t\t\t\t\t\tsize={20}\n\t\t\t\t\t\t\t\t\t\tclassName=\"text-gray-400\"\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t{metadata.website_name && (\n\t\t\t\t\t\t\t\t\t<div className=\"truncate text-sm font-semibold\">\n\t\t\t\t\t\t\t\t\t\t{metadata.website_name}\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)}\n\n\t\t\t\t\t\t{/* Title */}\n\t\t\t\t\t\t{metadata.title && (\n\t\t\t\t\t\t\t<div className=\"truncate text-sm font-medium text-white\">\n\t\t\t\t\t\t\t\t{metadata.title}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)}\n\n\t\t\t\t\t\t{/* Description */}\n\t\t\t\t\t\t{metadata.description && (\n\t\t\t\t\t\t\t<div className=\"line-clamp-3 text-xs text-gray-400 w-full\">\n\t\t\t\t\t\t\t\t{metadata.description}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)}\n\n\t\t\t\t\t\t{/* URL Link */}\n\t\t\t\t\t\t<div className=\"truncate text-xs text-primary\">\n\t\t\t\t\t\t\t{href.replace(\"https://\", \"\").replace(\"http://\", \"\")}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t) : (\n\t\t\t\t\t<div className=\"flex items-center gap-2 p-3\">\n\t\t\t\t\t\t<HugeiconsIcon\n\t\t\t\t\t\t\ticon={Globe02Icon}\n\t\t\t\t\t\t\tsize={16}\n\t\t\t\t\t\t\tclassName=\"text-gray-400\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<span className=\"text-sm text-gray-400\">No preview available</span>\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\t\t\t</TooltipContent>\n\t\t</Tooltip>\n\t);\n}\n",
      "type": "registry:ui"
    }
  ]
}