{
  "name": "developer-tools-artifactdisplay",
  "type": "registry:component",
  "dependencies": [
    "bitcoin-image",
    "lucide-react"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/developer-tools/ArtifactDisplay.tsx",
      "type": "registry:component",
      "content": "/**\n * ArtifactDisplay Component\n *\n * Universal content renderer for blockchain artifacts (ordinals, inscriptions, files)\n * Supports images, videos, audio, HTML, text, JSON, SVG, and more\n * Uses BitcoinImage component for blockchain URL processing\n */\n\nimport {\n\tCopyIcon,\n\tCrossCircledIcon,\n\tDownloadIcon,\n\tExternalLinkIcon,\n\tInfoCircledIcon,\n} from \"@radix-ui/react-icons\";\nimport { ImageProtocols } from \"bitcoin-image\";\nimport { Loader2 } from \"lucide-react\";\nimport React, { useState, useMemo, useCallback } from \"react\";\nimport { cn } from \"../../lib/utils.js\";\nimport { BitcoinImage } from \"../ui-components/BitcoinImage.js\";\nimport { Badge } from \"../ui/badge.js\";\nimport { Button } from \"../ui/button.js\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"../ui/card.js\";\nimport { Dialog, DialogContent, DialogOverlay } from \"../ui/dialog.js\";\nimport {\n\tTooltip,\n\tTooltipContent,\n\tTooltipProvider,\n\tTooltipTrigger,\n} from \"../ui/tooltip.js\";\n\n// Initialize bitcoin-image protocols\nconst imageProtocols = new ImageProtocols();\n\n// Types\nexport enum ArtifactType {\n\tImage = \"image\",\n\tVideo = \"video\",\n\tAudio = \"audio\",\n\tHTML = \"html\",\n\tText = \"text\",\n\tJSON = \"json\",\n\tSVG = \"svg\",\n\tPDF = \"pdf\",\n\tModel = \"model\",\n\tUnknown = \"unknown\",\n}\n\nexport interface ArtifactData {\n\t/** The URL or blockchain reference to the artifact */\n\tsrc: string;\n\t/** Content type (MIME type) */\n\tcontentType?: string;\n\t/** Artifact type (auto-detected if not provided) */\n\ttype?: ArtifactType;\n\t/** Size in bytes */\n\tsize?: number;\n\t/** Display name */\n\tname?: string;\n\t/** Description */\n\tdescription?: string;\n\t/** Transaction ID (for blockchain artifacts) */\n\ttxid?: string;\n\t/** Output index */\n\tvout?: number;\n\t/** Inscription number */\n\tnumber?: string | number;\n\t/** Additional metadata */\n\tmetadata?: Record<string, unknown>;\n}\n\nexport interface ArtifactDisplayProps {\n\t/** Artifact data */\n\tartifact: ArtifactData;\n\t/** Display size */\n\tsize?: number;\n\t/** Maximum width */\n\tmaxWidth?: number;\n\t/** Whether to show controls/footer */\n\tshowControls?: boolean;\n\t/** Whether to allow zooming */\n\tallowZoom?: boolean;\n\t/** Whether to show metadata */\n\tshowMetadata?: boolean;\n\t/** Loading state */\n\tloading?: boolean;\n\t/** Error state */\n\terror?: string;\n\t/** Custom CSS classes */\n\tclassName?: string;\n\t/** Click handler */\n\tonClick?: (artifact: ArtifactData) => void;\n\t/** Download handler */\n\tonDownload?: (artifact: ArtifactData) => void;\n\t/** Share handler */\n\tonShare?: (artifact: ArtifactData) => void;\n\t/** Custom fallback component */\n\tfallback?: React.ReactNode;\n}\n\n// Shared error display component\nconst ErrorDisplay: React.FC<{\n\tmessage: string;\n\tsize?: number;\n\tmaxWidth?: number;\n\tclassName?: string;\n}> = ({ message, size, maxWidth, className }) => (\n\t<div\n\t\tclassName={cn(\n\t\t\t\"flex flex-col items-center justify-center bg-muted border border-dashed border-muted-foreground/30 rounded-md\",\n\t\t\tclassName,\n\t\t)}\n\t\tstyle={{\n\t\t\twidth: size || maxWidth || 300,\n\t\t\theight: size || 200,\n\t\t}}\n\t>\n\t\t<CrossCircledIcon className=\"w-6 h-6 text-muted-foreground\" />\n\t\t<p className=\"mt-2 text-sm text-muted-foreground text-center\">{message}</p>\n\t</div>\n);\n\n// Custom hook for fetching content\nconst useContentFetch = (\n\tsrc: string,\n\tparser?: (response: Response) => Promise<unknown>,\n) => {\n\tconst [content, setContent] = useState<unknown>(\"\");\n\tconst [loading, setLoading] = useState(true);\n\tconst [error, setError] = useState<string>(\"\");\n\n\tReact.useEffect(() => {\n\t\tlet mounted = true;\n\n\t\tconst fetchContent = async () => {\n\t\t\ttry {\n\t\t\t\tsetLoading(true);\n\t\t\t\tsetError(\"\");\n\n\t\t\t\tconst response = await fetch(src);\n\t\t\t\tconst data = parser ? await parser(response) : await response.text();\n\n\t\t\t\tif (mounted) {\n\t\t\t\t\tsetContent(data);\n\t\t\t\t\tsetLoading(false);\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\tif (mounted) {\n\t\t\t\t\tsetError(\n\t\t\t\t\t\terr instanceof Error ? err.message : \"Failed to load content\",\n\t\t\t\t\t);\n\t\t\t\t\tsetLoading(false);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\n\t\tfetchContent();\n\n\t\treturn () => {\n\t\t\tmounted = false;\n\t\t};\n\t}, [src, parser]);\n\n\treturn { content, loading, error };\n};\n\n// Content type detection\nconst getArtifactTypeFromContentType = (contentType: string): ArtifactType => {\n\tif (contentType.startsWith(\"image/\")) {\n\t\treturn contentType === \"image/svg+xml\"\n\t\t\t? ArtifactType.SVG\n\t\t\t: ArtifactType.Image;\n\t}\n\tif (contentType.startsWith(\"video/\")) return ArtifactType.Video;\n\tif (contentType.startsWith(\"audio/\")) return ArtifactType.Audio;\n\tif (contentType === \"text/html\") return ArtifactType.HTML;\n\tif (contentType === \"application/json\") return ArtifactType.JSON;\n\tif (contentType.startsWith(\"text/\")) return ArtifactType.Text;\n\tif (contentType === \"application/pdf\") return ArtifactType.PDF;\n\tif (contentType.startsWith(\"model/\")) return ArtifactType.Model;\n\treturn ArtifactType.Unknown;\n};\n\n// Image component using BitcoinImage\nconst ArtifactImage: React.FC<{\n\tsrc: string;\n\talt: string;\n\tsize?: number;\n\tmaxWidth?: number;\n\tallowZoom?: boolean;\n\tclassName?: string;\n\tonClick?: () => void;\n}> = ({ src, alt, size, maxWidth, allowZoom, className, onClick }) => {\n\tconst errorFallback = (\n\t\t<ErrorDisplay\n\t\t\tmessage=\"Failed to load image\"\n\t\t\tsize={size}\n\t\t\tmaxWidth={maxWidth}\n\t\t\tclassName={className}\n\t\t/>\n\t);\n\n\treturn (\n\t\t<div className={className} style={{ position: \"relative\" }}>\n\t\t\t<BitcoinImage\n\t\t\t\tsrc={src}\n\t\t\t\talt={alt}\n\t\t\t\tstyle={{\n\t\t\t\t\tmaxWidth: maxWidth || \"100%\",\n\t\t\t\t\twidth: size ? `${size}px` : \"auto\",\n\t\t\t\t\theight: size ? `${size}px` : \"auto\",\n\t\t\t\t\tobjectFit: \"contain\",\n\t\t\t\t\tcursor: allowZoom || onClick ? \"pointer\" : \"default\",\n\t\t\t\t}}\n\t\t\t\tfallback={errorFallback}\n\t\t\t\ttimeout={10000}\n\t\t\t/>\n\t\t\t{/* Overlay for click handling since BitcoinImage doesn't expose click events */}\n\t\t\t{(allowZoom || onClick) && (\n\t\t\t\t<div\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\t\ttop: 0,\n\t\t\t\t\t\tleft: 0,\n\t\t\t\t\t\tright: 0,\n\t\t\t\t\t\tbottom: 0,\n\t\t\t\t\t\tcursor: \"pointer\",\n\t\t\t\t\t}}\n\t\t\t\t\tonClick={onClick}\n\t\t\t\t\tonKeyDown={(e) => {\n\t\t\t\t\t\tif ((e.key === \"Enter\" || e.key === \" \") && onClick) {\n\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\tonClick();\n\t\t\t\t\t\t}\n\t\t\t\t\t}}\n\t\t\t\t\ttabIndex={onClick ? 0 : -1}\n\t\t\t\t\trole={onClick ? \"button\" : undefined}\n\t\t\t\t/>\n\t\t\t)}\n\t\t</div>\n\t);\n};\n\n// Video component\nconst ArtifactVideo: React.FC<{\n\tsrc: string;\n\tmaxWidth?: number;\n\tclassName?: string;\n}> = ({ src, maxWidth, className }) => (\n\t<video\n\t\tsrc={src}\n\t\tcontrols\n\t\tstyle={{ maxWidth: maxWidth || \"100%\", width: \"100%\" }}\n\t\tclassName={className}\n\t>\n\t\t<track kind=\"captions\" src=\"\" label=\"No captions available\" />\n\t\tYour browser does not support video playback.\n\t</video>\n);\n\n// Audio component\nconst ArtifactAudio: React.FC<{\n\tsrc: string;\n\tclassName?: string;\n}> = ({ src, className }) => (\n\t<audio src={src} controls style={{ width: \"100%\" }} className={className}>\n\t\t<track kind=\"captions\" src=\"\" label=\"No captions available\" />\n\t\tYour browser does not support audio playback.\n\t</audio>\n);\n\n// Text component\nconst ArtifactText: React.FC<{\n\tsrc: string;\n\tmaxWidth?: number;\n\tclassName?: string;\n}> = ({ src, maxWidth, className }) => {\n\tconst { content, loading, error } = useContentFetch(src);\n\n\tif (loading) return <Loader2 className=\"h-8 w-8 animate-spin\" />;\n\tif (error) return <ErrorDisplay message={`Error loading text: ${error}`} />;\n\n\treturn (\n\t\t<div\n\t\t\tclassName={cn(\n\t\t\t\t\"max-h-[400px] overflow-auto p-4 bg-muted rounded-md font-mono text-sm\",\n\t\t\t\tclassName,\n\t\t\t)}\n\t\t\tstyle={{\n\t\t\t\tmaxWidth: maxWidth || \"100%\",\n\t\t\t}}\n\t\t>\n\t\t\t<pre className=\"m-0 whitespace-pre-wrap\">{content as string}</pre>\n\t\t</div>\n\t);\n};\n\n// JSON component\nconst ArtifactJSON: React.FC<{\n\tsrc: string;\n\tmaxWidth?: number;\n\tclassName?: string;\n}> = ({ src, maxWidth, className }) => {\n\tconst { content, loading, error } = useContentFetch(src, (response) =>\n\t\tresponse.json(),\n\t);\n\n\tif (loading) return <Loader2 className=\"h-8 w-8 animate-spin\" />;\n\tif (error) return <ErrorDisplay message={`Error loading JSON: ${error}`} />;\n\n\treturn (\n\t\t<div\n\t\t\tclassName={cn(\n\t\t\t\t\"max-h-[400px] overflow-auto p-4 bg-muted rounded-md\",\n\t\t\t\tclassName,\n\t\t\t)}\n\t\t\tstyle={{\n\t\t\t\tmaxWidth: maxWidth || \"100%\",\n\t\t\t}}\n\t\t>\n\t\t\t<pre className=\"m-0 text-sm font-mono\">\n\t\t\t\t{JSON.stringify(content, null, 2)}\n\t\t\t</pre>\n\t\t</div>\n\t);\n};\n\n// HTML iframe component\nconst ArtifactHTML: React.FC<{\n\tsrc: string;\n\tsize?: number;\n\tmaxWidth?: number;\n\tclassName?: string;\n}> = ({ src, size, maxWidth, className }) => (\n\t<iframe\n\t\tsrc={src}\n\t\tstyle={{\n\t\t\twidth: size ? `${size}px` : \"100%\",\n\t\t\theight: size ? `${size}px` : \"400px\",\n\t\t\tmaxWidth: maxWidth || \"100%\",\n\t\t}}\n\t\tclassName={cn(\"border border-border rounded-md\", className)}\n\t\tsandbox=\"allow-scripts allow-same-origin\"\n\t\ttitle=\"HTML Artifact\"\n\t/>\n);\n\n// Main component\nexport function ArtifactDisplay({\n\tartifact,\n\tsize,\n\tmaxWidth = 600,\n\tshowControls = true,\n\tallowZoom = true,\n\tshowMetadata = false,\n\tloading = false,\n\terror,\n\tclassName = \"\",\n\tonClick,\n\tonDownload,\n\tonShare,\n\tfallback,\n}: ArtifactDisplayProps) {\n\tconst [isZoomed, setIsZoomed] = useState(false);\n\n\t// Determine artifact type with memoization\n\tconst artifactType = useMemo(() => {\n\t\tif (artifact.type) return artifact.type;\n\t\tif (artifact.contentType)\n\t\t\treturn getArtifactTypeFromContentType(artifact.contentType);\n\n\t\t// Use bitcoin-image library to parse and validate URLs\n\t\tconst parsed = imageProtocols.parse(artifact.src);\n\t\tif (parsed.isValid) {\n\t\t\treturn ArtifactType.Image;\n\t\t}\n\n\t\t// Fallback to file extension detection for non-blockchain URLs\n\t\ttry {\n\t\t\tconst extension = artifact.src.split(\".\").pop()?.toLowerCase();\n\t\t\tconst extensionMap: Record<string, ArtifactType> = {\n\t\t\t\tjpg: ArtifactType.Image,\n\t\t\t\tjpeg: ArtifactType.Image,\n\t\t\t\tpng: ArtifactType.Image,\n\t\t\t\tgif: ArtifactType.Image,\n\t\t\t\twebp: ArtifactType.Image,\n\t\t\t\tsvg: ArtifactType.SVG,\n\t\t\t\tmp4: ArtifactType.Video,\n\t\t\t\twebm: ArtifactType.Video,\n\t\t\t\tmov: ArtifactType.Video,\n\t\t\t\tmp3: ArtifactType.Audio,\n\t\t\t\twav: ArtifactType.Audio,\n\t\t\t\togg: ArtifactType.Audio,\n\t\t\t\thtml: ArtifactType.HTML,\n\t\t\t\thtm: ArtifactType.HTML,\n\t\t\t\tjson: ArtifactType.JSON,\n\t\t\t\ttxt: ArtifactType.Text,\n\t\t\t\tmd: ArtifactType.Text,\n\t\t\t\tpdf: ArtifactType.PDF,\n\t\t\t};\n\t\t\treturn extensionMap[extension || \"\"] || ArtifactType.Unknown;\n\t\t} catch {\n\t\t\treturn ArtifactType.Unknown;\n\t\t}\n\t}, [artifact.type, artifact.contentType, artifact.src]);\n\n\t// Handle click\n\tconst handleClick = useCallback(() => {\n\t\tif (onClick) {\n\t\t\tonClick(artifact);\n\t\t} else if (allowZoom && artifactType === ArtifactType.Image) {\n\t\t\tsetIsZoomed(!isZoomed);\n\t\t}\n\t}, [onClick, artifact, allowZoom, artifactType, isZoomed]);\n\n\t// Handle download with URL conversion\n\tconst handleDownload = useCallback(async () => {\n\t\tif (onDownload) {\n\t\t\tonDownload(artifact);\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst downloadUrl = await imageProtocols.getDisplayUrl(artifact.src);\n\t\t\tconst link = document.createElement(\"a\");\n\t\t\tlink.href = downloadUrl;\n\t\t\tlink.download = artifact.name || \"artifact\";\n\t\t\tlink.click();\n\t\t} catch {\n\t\t\t// Fallback to original URL\n\t\t\tconst link = document.createElement(\"a\");\n\t\t\tlink.href = artifact.src;\n\t\t\tlink.download = artifact.name || \"artifact\";\n\t\t\tlink.click();\n\t\t}\n\t}, [onDownload, artifact]);\n\n\t// Handle share with URL conversion\n\tconst handleShare = useCallback(async () => {\n\t\tif (onShare) {\n\t\t\tonShare(artifact);\n\t\t\treturn;\n\t\t}\n\n\t\t// Get display URL for sharing\n\t\tlet shareUrl = artifact.src;\n\t\ttry {\n\t\t\tshareUrl = await imageProtocols.getDisplayUrl(artifact.src);\n\t\t} catch {\n\t\t\t// Use original URL if conversion fails\n\t\t}\n\n\t\tif (navigator.share) {\n\t\t\ttry {\n\t\t\t\tawait navigator.share({\n\t\t\t\t\ttitle: artifact.name || \"Blockchain Artifact\",\n\t\t\t\t\ttext: artifact.description,\n\t\t\t\t\turl: shareUrl,\n\t\t\t\t});\n\t\t\t} catch {\n\t\t\t\tnavigator.clipboard?.writeText(shareUrl);\n\t\t\t}\n\t\t} else {\n\t\t\tnavigator.clipboard?.writeText(shareUrl);\n\t\t}\n\t}, [onShare, artifact]);\n\n\t// Handle view original\n\tconst handleViewOriginal = useCallback(async () => {\n\t\ttry {\n\t\t\tconst displayUrl = await imageProtocols.getDisplayUrl(artifact.src);\n\t\t\twindow.open(displayUrl, \"_blank\");\n\t\t} catch {\n\t\t\twindow.open(artifact.src, \"_blank\");\n\t\t}\n\t}, [artifact.src]);\n\n\t// Render content based on type\n\tconst renderContent = () => {\n\t\tif (loading) {\n\t\t\treturn (\n\t\t\t\t<div\n\t\t\t\t\tclassName=\"flex items-center justify-center\"\n\t\t\t\t\tstyle={{ height: size || 200 }}\n\t\t\t\t>\n\t\t\t\t\t<Loader2 className=\"h-8 w-8 animate-spin\" />\n\t\t\t\t\t<div style={{ marginLeft: \"0.5rem\" }}>\n\t\t\t\t\t\t<span className=\"text-sm text-muted-foreground\">\n\t\t\t\t\t\t\tLoading artifact...\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t);\n\t\t}\n\n\t\tif (error) {\n\t\t\treturn <ErrorDisplay message={error} size={size} />;\n\t\t}\n\n\t\tif (fallback && artifactType === ArtifactType.Unknown) {\n\t\t\treturn fallback;\n\t\t}\n\n\t\tconst contentProps = {\n\t\t\tsrc: artifact.src,\n\t\t\tmaxWidth,\n\t\t\tclassName: \"artifact-content\",\n\t\t};\n\n\t\tswitch (artifactType) {\n\t\t\tcase ArtifactType.Image:\n\t\t\tcase ArtifactType.SVG:\n\t\t\t\treturn (\n\t\t\t\t\t<ArtifactImage\n\t\t\t\t\t\t{...contentProps}\n\t\t\t\t\t\talt={artifact.name || \"Blockchain artifact\"}\n\t\t\t\t\t\tsize={size}\n\t\t\t\t\t\tallowZoom={allowZoom}\n\t\t\t\t\t\tonClick={handleClick}\n\t\t\t\t\t/>\n\t\t\t\t);\n\n\t\t\tcase ArtifactType.Video:\n\t\t\t\treturn <ArtifactVideo {...contentProps} />;\n\n\t\t\tcase ArtifactType.Audio:\n\t\t\t\treturn <ArtifactAudio {...contentProps} />;\n\n\t\t\tcase ArtifactType.Text:\n\t\t\t\treturn <ArtifactText {...contentProps} />;\n\n\t\t\tcase ArtifactType.JSON:\n\t\t\t\treturn <ArtifactJSON {...contentProps} />;\n\n\t\t\tcase ArtifactType.HTML:\n\t\t\t\treturn <ArtifactHTML {...contentProps} size={size} />;\n\n\t\t\tcase ArtifactType.PDF:\n\t\t\t\treturn (\n\t\t\t\t\t<div className=\"flex flex-col items-center p-4\">\n\t\t\t\t\t\t<div style={{ marginBottom: \"0.75rem\" }}>\n\t\t\t\t\t\t\t<span className=\"text-sm text-muted-foreground\">PDF Preview</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<Button variant=\"secondary\" onClick={handleDownload}>\n\t\t\t\t\t\t\t<DownloadIcon width=\"16\" height=\"16\" />\n\t\t\t\t\t\t\tDownload PDF\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</div>\n\t\t\t\t);\n\n\t\t\tdefault:\n\t\t\t\treturn (\n\t\t\t\t\t<div\n\t\t\t\t\t\tclassName=\"flex flex-col items-center justify-center p-4\"\n\t\t\t\t\t\tstyle={{ height: size || 200 }}\n\t\t\t\t\t>\n\t\t\t\t\t\t<InfoCircledIcon className=\"w-6 h-6 text-muted-foreground\" />\n\t\t\t\t\t\t<div className=\"mt-2\">\n\t\t\t\t\t\t\t<span className=\"text-sm text-muted-foreground text-center\">\n\t\t\t\t\t\t\t\tUnknown artifact type\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div className=\"mt-1\">\n\t\t\t\t\t\t\t<span className=\"text-xs text-muted-foreground\">\n\t\t\t\t\t\t\t\t{artifact.contentType || \"No content type specified\"}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t);\n\t\t}\n\t};\n\n\treturn (\n\t\t<>\n\t\t\t<Card className={className}>\n\t\t\t\t<CardContent className=\"p-4\">\n\t\t\t\t\t<div className=\"flex flex-col gap-3\">\n\t\t\t\t\t\t{/* Content */}\n\t\t\t\t\t\t<div className=\"relative\">{renderContent()}</div>\n\n\t\t\t\t\t\t{/* Controls */}\n\t\t\t\t\t\t{showControls && (\n\t\t\t\t\t\t\t<div className=\"flex justify-between items-center pt-2\">\n\t\t\t\t\t\t\t\t<div className=\"flex items-center gap-2\">\n\t\t\t\t\t\t\t\t\t<Badge variant=\"secondary\">\n\t\t\t\t\t\t\t\t\t\t{artifactType.toUpperCase()}\n\t\t\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t\t\t{artifact.number && (\n\t\t\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\t\t#{artifact.number}\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t{artifact.size && (\n\t\t\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\t\t{(artifact.size / 1024).toFixed(1)}KB\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t\t<TooltipProvider>\n\t\t\t\t\t\t\t\t\t<div className=\"flex gap-1\">\n\t\t\t\t\t\t\t\t\t\t<Tooltip>\n\t\t\t\t\t\t\t\t\t\t\t<TooltipTrigger asChild>\n\t\t\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tonClick={handleViewOriginal}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<ExternalLinkIcon className=\"w-3.5 h-3.5\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t</TooltipTrigger>\n\t\t\t\t\t\t\t\t\t\t\t<TooltipContent>View original</TooltipContent>\n\t\t\t\t\t\t\t\t\t\t</Tooltip>\n\n\t\t\t\t\t\t\t\t\t\t<Tooltip>\n\t\t\t\t\t\t\t\t\t\t\t<TooltipTrigger asChild>\n\t\t\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tonClick={handleDownload}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<DownloadIcon className=\"w-3.5 h-3.5\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t</TooltipTrigger>\n\t\t\t\t\t\t\t\t\t\t\t<TooltipContent>Download</TooltipContent>\n\t\t\t\t\t\t\t\t\t\t</Tooltip>\n\n\t\t\t\t\t\t\t\t\t\t<Tooltip>\n\t\t\t\t\t\t\t\t\t\t\t<TooltipTrigger asChild>\n\t\t\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tonClick={handleShare}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<CopyIcon className=\"w-3.5 h-3.5\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t</TooltipTrigger>\n\t\t\t\t\t\t\t\t\t\t\t<TooltipContent>Share</TooltipContent>\n\t\t\t\t\t\t\t\t\t\t</Tooltip>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</TooltipProvider>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)}\n\n\t\t\t\t\t\t{/* Metadata */}\n\t\t\t\t\t\t{showMetadata &&\n\t\t\t\t\t\t\t(artifact.name || artifact.description || artifact.txid) && (\n\t\t\t\t\t\t\t\t<div className=\"flex flex-col gap-2 pt-2 border-t border-border\">\n\t\t\t\t\t\t\t\t\t{artifact.name && (\n\t\t\t\t\t\t\t\t\t\t<p className=\"text-sm font-medium\">{artifact.name}</p>\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t{artifact.description && (\n\t\t\t\t\t\t\t\t\t\t<p className=\"text-sm text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t\t\t{artifact.description}\n\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t{artifact.txid && (\n\t\t\t\t\t\t\t\t\t\t<p className=\"text-xs text-muted-foreground font-mono\">\n\t\t\t\t\t\t\t\t\t\t\t{artifact.txid.slice(0, 16)}...{artifact.txid.slice(-8)}\n\t\t\t\t\t\t\t\t\t\t\t{artifact.vout !== undefined && `_${artifact.vout}`}\n\t\t\t\t\t\t\t\t\t\t</p>\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</div>\n\t\t\t\t</CardContent>\n\t\t\t</Card>\n\n\t\t\t{/* Zoom modal */}\n\t\t\t<Dialog\n\t\t\t\topen={isZoomed && artifactType === ArtifactType.Image}\n\t\t\t\tonOpenChange={setIsZoomed}\n\t\t\t>\n\t\t\t\t<DialogOverlay />\n\t\t\t\t<DialogContent className=\"max-w-[95vw] w-full h-[90vh] flex items-center justify-center p-0 bg-transparent border-0\">\n\t\t\t\t\t<BitcoinImage\n\t\t\t\t\t\tsrc={artifact.src}\n\t\t\t\t\t\talt={artifact.name || \"Zoomed artifact\"}\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\tmaxWidth: \"90vw\",\n\t\t\t\t\t\t\tmaxHeight: \"90vh\",\n\t\t\t\t\t\t\tobjectFit: \"contain\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tfallback={\n\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\tsrc={artifact.src}\n\t\t\t\t\t\t\t\talt={artifact.name || \"Zoomed artifact\"}\n\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\tmaxWidth: \"90vw\",\n\t\t\t\t\t\t\t\t\tmaxHeight: \"90vh\",\n\t\t\t\t\t\t\t\t\tobjectFit: \"contain\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t}\n\t\t\t\t\t/>\n\t\t\t\t</DialogContent>\n\t\t\t</Dialog>\n\t\t</>\n\t);\n}\n",
      "target": "<%- config.aliases.components %>/artifactdisplay.tsx"
    }
  ]
}