{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "search-results-tabs",
  "type": "registry:ui",
  "title": "Search Results Tabs",
  "description": "A beautiful component for displaying search results with web pages, images, and news in an organized, interactive layout.",
  "dependencies": [
    "framer-motion"
  ],
  "registryDependencies": [
    "icons"
  ],
  "files": [
    {
      "path": "registry/new-york/ui/search-results-tabs.tsx",
      "content": "\"use client\";\n\nimport { motion } from \"framer-motion\";\nimport Image from \"next/image\";\nimport { useCallback, useEffect, useState } from \"react\";\nimport { HugeiconsIcon, News01Icon } from \"@/components/icons\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n\tPopover,\n\tPopoverContent,\n\tPopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\n\nexport interface ImageResult {\n\turl: string;\n\ttitle?: string;\n}\n\nexport interface WebResult {\n\ttitle: string;\n\turl: string;\n\tcontent: string;\n\tdate?: string;\n}\n\nexport interface NewsResult {\n\ttitle: string;\n\turl: string;\n\tcontent: string;\n\tscore?: number;\n\tdate?: string;\n}\n\nexport interface SearchResults {\n\tweb?: WebResult[];\n\timages?: string[] | ImageResult[];\n\tnews?: NewsResult[];\n}\n\ninterface SearchResultsTabsProps {\n\tsearch_results: SearchResults;\n\tonImageClick?: (imageUrl: string) => void;\n}\n\nexport function SearchResultsTabs({\n\tsearch_results,\n\tonImageClick,\n}: SearchResultsTabsProps) {\n\treturn (\n\t\t<div className=\"w-full\">\n\t\t\t<div className=\"space-y-6\">\n\t\t\t\t{search_results.web && search_results.web?.length > 0 && (\n\t\t\t\t\t<SourcesButton web={search_results.web} />\n\t\t\t\t)}\n\n\t\t\t\t{search_results.images && search_results.images?.length > 0 && (\n\t\t\t\t\t<ImageResults\n\t\t\t\t\t\timages={search_results.images}\n\t\t\t\t\t\tonImageClick={onImageClick}\n\t\t\t\t\t/>\n\t\t\t\t)}\n\n\t\t\t\t{search_results.news && search_results.news?.length > 0 && (\n\t\t\t\t\t<NewsResults news={search_results.news} />\n\t\t\t\t)}\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n\ninterface ImageResultsProps {\n\timages: string[] | ImageResult[];\n\tonImageClick?: (imageUrl: string) => void;\n}\n\nfunction ImageResults({ images, onImageClick }: ImageResultsProps) {\n\tconst [validImages, setValidImages] = useState<string[]>([]);\n\n\tuseEffect(() => {\n\t\tconst validateImages = async () => {\n\t\t\t// Convert to string array if needed\n\t\t\tconst imageUrls = images.map((img) =>\n\t\t\t\ttypeof img === \"string\" ? img : img.url,\n\t\t\t);\n\n\t\t\t// Filter out obviously invalid images first\n\t\t\tconst potentiallyValidImages = imageUrls.filter(\n\t\t\t\t(imageUrl) => imageUrl && typeof imageUrl === \"string\",\n\t\t\t);\n\n\t\t\t// Test each image by trying to load it\n\t\t\tconst validationPromises = potentiallyValidImages.map(\n\t\t\t\t(imageUrl) =>\n\t\t\t\t\tnew Promise<string | null>((resolve) => {\n\t\t\t\t\t\tconst img = new window.Image();\n\n\t\t\t\t\t\tconst timeoutId = setTimeout(() => {\n\t\t\t\t\t\t\tresolve(null); // Timeout after 5 seconds\n\t\t\t\t\t\t}, 5000);\n\n\t\t\t\t\t\timg.onload = () => {\n\t\t\t\t\t\t\tclearTimeout(timeoutId);\n\t\t\t\t\t\t\tresolve(imageUrl);\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\timg.onerror = () => {\n\t\t\t\t\t\t\tclearTimeout(timeoutId);\n\t\t\t\t\t\t\tresolve(null);\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\timg.src = imageUrl;\n\t\t\t\t\t}),\n\t\t\t);\n\n\t\t\ttry {\n\t\t\t\tconst results = await Promise.all(validationPromises);\n\t\t\t\tconst validImageUrls = results.filter(\n\t\t\t\t\t(url): url is string => url !== null,\n\t\t\t\t);\n\t\t\t\tsetValidImages(validImageUrls);\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\"Error validating images:\", error);\n\t\t\t\tsetValidImages([]);\n\t\t\t}\n\t\t};\n\n\t\tif (images && images.length > 0) validateImages();\n\t\telse setValidImages([]);\n\t}, [images]);\n\n\tif (validImages.length === 0) {\n\t\treturn null;\n\t}\n\n\treturn (\n\t\t<div className=\"my-4 flex w-full max-w-2xl -space-x-15 pr-2\">\n\t\t\t{validImages.map((imageUrl, index) => (\n\t\t\t\t<ImageItem\n\t\t\t\t\tkey={imageUrl}\n\t\t\t\t\timageUrl={imageUrl}\n\t\t\t\t\tindex={index}\n\t\t\t\t\tonImageClick={() => onImageClick?.(imageUrl)}\n\t\t\t\t\ttotalImages={validImages.length}\n\t\t\t\t/>\n\t\t\t))}\n\t\t</div>\n\t);\n}\n\ninterface ImageItemProps {\n\timageUrl: string;\n\tindex: number;\n\tonImageClick: () => void;\n\ttotalImages: number;\n}\n\nfunction ImageItem({\n\timageUrl,\n\tindex,\n\tonImageClick,\n\ttotalImages,\n}: ImageItemProps) {\n\tconst [isLoading, setIsLoading] = useState(true);\n\n\tconst handleLoad = useCallback(() => {\n\t\tsetIsLoading(false);\n\t}, []);\n\n\treturn (\n\t\t<motion.div\n\t\t\tonClick={onImageClick}\n\t\t\tclassName={`group cursor-pointer overflow-hidden rounded-2xl shadow-lg transition-all duration-200 ${\n\t\t\t\t(index + 1) % 2 === 0\n\t\t\t\t\t? \"-rotate-7 hover:-rotate-0\"\n\t\t\t\t\t: \"rotate-7 hover:rotate-0\"\n\t\t\t}`}\n\t\t\tstyle={{\n\t\t\t\tzIndex: index,\n\t\t\t}}\n\t\t\tinitial={{ scale: 0.6, filter: \"blur(10px)\" }}\n\t\t\tanimate={{ scale: 1, filter: \"blur(0px)\" }}\n\t\t\ttransition={{\n\t\t\t\tdelay: index * 0.1,\n\t\t\t\tduration: 0.1,\n\t\t\t\tease: [0.19, 1, 0.22, 1],\n\t\t\t\tscale: {\n\t\t\t\t\tduration: 0.2,\n\t\t\t\t},\n\t\t\t}}\n\t\t\tonMouseEnter={(e) => {\n\t\t\t\te.currentTarget.style.zIndex = (totalImages + 10).toString();\n\t\t\t}}\n\t\t\tonMouseLeave={(e) => {\n\t\t\t\te.currentTarget.style.zIndex = index.toString();\n\t\t\t}}\n\t\t>\n\t\t\t{isLoading && (\n\t\t\t\t<div className=\"absolute inset-0 z-10\">\n\t\t\t\t\t<Skeleton className=\"aspect-square h-full w-full rounded-2xl\" />\n\t\t\t\t</div>\n\t\t\t)}\n\t\t\t<Image\n\t\t\t\tsrc={imageUrl}\n\t\t\t\talt={`Search result image ${index + 1}`}\n\t\t\t\twidth={700}\n\t\t\t\theight={700}\n\t\t\t\tclassName={`aspect-square h-full bg-zinc-800 object-cover transition ${\n\t\t\t\t\tisLoading ? \"opacity-0\" : \"opacity-100\"\n\t\t\t\t}`}\n\t\t\t\tonLoad={handleLoad}\n\t\t\t\tpriority={index < 3}\n\t\t\t/>\n\t\t</motion.div>\n\t);\n}\n\ninterface SourcesButtonProps {\n\tweb: WebResult[];\n}\n\nfunction SourcesButton({ web }: SourcesButtonProps) {\n\treturn (\n\t\t<div className=\"flex justify-start\">\n\t\t\t<Popover>\n\t\t\t\t<PopoverTrigger asChild>\n\t\t\t\t\t<Button variant=\"secondary\" size=\"sm\" className=\"rounded-full\">\n\t\t\t\t\t\t<div className=\"flex -space-x-3\">\n\t\t\t\t\t\t\t{web.slice(0, 4).map((result, index) => (\n\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\tkey={index}\n\t\t\t\t\t\t\t\t\tclassName=\"flex h-5 w-5 items-center justify-center rounded-full border-2 border-background bg-muted\"\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<Image\n\t\t\t\t\t\t\t\t\t\tsrc={`https://www.google.com/s2/favicons?domain=${\n\t\t\t\t\t\t\t\t\t\t\tnew URL(result.url).hostname\n\t\t\t\t\t\t\t\t\t\t}&sz=64`}\n\t\t\t\t\t\t\t\t\t\talt={`${new URL(result.url).hostname} favicon`}\n\t\t\t\t\t\t\t\t\t\twidth={16}\n\t\t\t\t\t\t\t\t\t\theight={16}\n\t\t\t\t\t\t\t\t\t\tclassName=\"h-full w-full rounded-full\"\n\t\t\t\t\t\t\t\t\t\tonError={(e) => {\n\t\t\t\t\t\t\t\t\t\t\tconst target = e.target as HTMLImageElement;\n\t\t\t\t\t\t\t\t\t\t\ttarget.style.display = \"none\";\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<span className=\"font-medium\">Search Results</span>\n\t\t\t\t\t</Button>\n\t\t\t\t</PopoverTrigger>\n\t\t\t\t<PopoverContent className=\"w-full max-w-lg bg-background/95 p-0 backdrop-blur-xl\">\n\t\t\t\t\t<WebResults web={web} />\n\t\t\t\t</PopoverContent>\n\t\t\t</Popover>\n\t\t</div>\n\t);\n}\n\ninterface NewsResultsProps {\n\tnews: NewsResult[];\n}\n\nfunction NewsResults({ news }: NewsResultsProps) {\n\treturn (\n\t\t<div className=\"space-y-2\">\n\t\t\t{news.map((article, index) => (\n\t\t\t\t<div\n\t\t\t\t\tkey={index}\n\t\t\t\t\tclassName=\"overflow-hidden rounded-lg border bg-card p-4 shadow-sm transition-all hover:shadow-md\"\n\t\t\t\t>\n\t\t\t\t\t<div className=\"flex flex-row items-center gap-2 transition-all hover:text-primary\">\n\t\t\t\t\t\t<HugeiconsIcon icon={News01Icon} size={20} />\n\t\t\t\t\t\t<h2 className=\"truncate text-lg font-medium\">\n\t\t\t\t\t\t\t<a href={article.url} target=\"_blank\" rel=\"noopener noreferrer\">\n\t\t\t\t\t\t\t\t{article.title}\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t</h2>\n\t\t\t\t\t</div>\n\t\t\t\t\t<p className=\"mb-1 line-clamp-2 text-sm text-muted-foreground\">\n\t\t\t\t\t\t{article.content}\n\t\t\t\t\t</p>\n\t\t\t\t\t<div className=\"flex flex-wrap items-center gap-x-4 text-sm text-muted-foreground\">\n\t\t\t\t\t\t{article.score && (\n\t\t\t\t\t\t\t<span className=\"text-xs\">Score: {article.score.toFixed(2)}</span>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{article.date && (\n\t\t\t\t\t\t\t<span className=\"text-xs\">\n\t\t\t\t\t\t\t\t{new Date(article.date).toLocaleDateString()}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t))}\n\t\t</div>\n\t);\n}\n\ninterface WebResultsProps {\n\tweb: WebResult[];\n}\n\nfunction WebResults({ web }: WebResultsProps) {\n\treturn (\n\t\t<div className=\"max-h-80 w-full overflow-y-auto\">\n\t\t\t{web.map((result, index) => (\n\t\t\t\t<div\n\t\t\t\t\tclassName=\"w-full border-b p-4 pb-3 transition-all hover:bg-accent/50\"\n\t\t\t\t\tkey={index}\n\t\t\t\t>\n\t\t\t\t\t<a\n\t\t\t\t\t\thref={result.url}\n\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\tclassName=\"w-full space-y-1\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<h2 className=\"truncate text-sm font-medium\">{result.title}</h2>\n\t\t\t\t\t\t<p className=\"line-clamp-2 text-xs text-muted-foreground\">\n\t\t\t\t\t\t\t{result.content}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t\t<div className=\"flex flex-wrap items-center gap-x-4 text-sm\">\n\t\t\t\t\t\t\t<span className=\"flex items-center gap-2\">\n\t\t\t\t\t\t\t\t<Image\n\t\t\t\t\t\t\t\t\tsrc={`https://www.google.com/s2/favicons?domain=${\n\t\t\t\t\t\t\t\t\t\tnew URL(result.url).hostname\n\t\t\t\t\t\t\t\t\t}&sz=64`}\n\t\t\t\t\t\t\t\t\talt={`${new URL(result.url).hostname} favicon`}\n\t\t\t\t\t\t\t\t\twidth={16}\n\t\t\t\t\t\t\t\t\theight={16}\n\t\t\t\t\t\t\t\t\tclassName=\"rounded-full\"\n\t\t\t\t\t\t\t\t\tonError={(e) => {\n\t\t\t\t\t\t\t\t\t\tconst target = e.target as HTMLImageElement;\n\t\t\t\t\t\t\t\t\t\ttarget.style.display = \"none\";\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<span className=\"max-w-xs truncate text-xs text-primary hover:underline\">\n\t\t\t\t\t\t\t\t\t{new URL(result.url).hostname}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t{result.date && (\n\t\t\t\t\t\t\t\t<span className=\"text-xs text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t{new Date(result.date).toLocaleDateString()}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</a>\n\t\t\t\t</div>\n\t\t\t))}\n\t\t</div>\n\t);\n}\n",
      "type": "registry:ui"
    }
  ]
}