{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file-thumbnail",
  "title": "File Thumbnail",
  "description": "A compact file preview thumbnail shell for externally generated images, loading states, and fallback states.",
  "files": [
    {
      "path": "components/extend/file-thumbnail.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nexport type ThumbnailFile = {\n  name: string\n  type: string\n}\nexport type FileThumbnailProps = {\n  file: ThumbnailFile | File\n  className?: string\n  previewAspectRatio?: number\n  previewClassName?: string\n  previewContent?: React.ReactNode\n  previewImageUrl?: string | null\n  isLoading?: boolean\n  hasError?: boolean\n}\nfunction cx(...classes: Array<string | false | null | undefined>) {\n  return classes.filter(Boolean).join(\" \")\n}\n// Preview URLs that have completed a reveal this session. View/tab switches\n// remount thumbnails; URLs in this set render instantly instead of replaying\n// the blur-in, so only an image's first load animates.\nconst revealedPreviewImageUrls = new Set<string>()\nexport function FileThumbnailLoadingOverlay() {\n  return (\n    <div\n      aria-hidden=\"true\"\n      className=\"absolute inset-0 z-10 overflow-hidden bg-muted\"\n    >\n      <div className=\"absolute inset-0 bg-muted\" />\n      <div className=\"absolute inset-0 animate-pulse bg-background/55 motion-reduce:animate-none\" />\n    </div>\n  )\n}\nexport function FileThumbnail({\n  className,\n  previewAspectRatio,\n  previewClassName,\n  previewContent,\n  previewImageUrl,\n  isLoading = false,\n  hasError = false,\n}: FileThumbnailProps) {\n  const imageRef = React.useRef<HTMLImageElement | null>(null)\n  const revealFrameRef = React.useRef<number | null>(null)\n  const [loadedPreviewImageUrl, setLoadedPreviewImageUrl] = React.useState<\n    string | null\n  >(() =>\n    previewImageUrl && revealedPreviewImageUrls.has(previewImageUrl)\n      ? previewImageUrl\n      : null\n  )\n  const [failedPreviewImageUrl, setFailedPreviewImageUrl] = React.useState<\n    string | null\n  >(null)\n  const imageFailed = Boolean(\n    previewImageUrl && failedPreviewImageUrl === previewImageUrl\n  )\n  const isImageLoading = Boolean(\n    previewImageUrl &&\n      loadedPreviewImageUrl !== previewImageUrl &&\n      !imageFailed &&\n      !revealedPreviewImageUrls.has(previewImageUrl)\n  )\n  const showLoading = isLoading || isImageLoading\n  const hasPreviewContent = Boolean(previewContent)\n  const showFallback =\n    !showLoading &&\n    (hasError || imageFailed || (!previewImageUrl && !hasPreviewContent))\n  const cancelImageReveal = React.useCallback(() => {\n    if (revealFrameRef.current === null) return\n    window.cancelAnimationFrame(revealFrameRef.current)\n    revealFrameRef.current = null\n  }, [])\n  const markImageLoaded = React.useCallback(\n    (image: HTMLImageElement, imageUrl: string | null | undefined) => {\n      if (!imageUrl) return\n      const didLoad = image.naturalWidth > 0 && image.naturalHeight > 0\n      setFailedPreviewImageUrl(didLoad ? null : imageUrl)\n      if (didLoad) {\n        revealedPreviewImageUrls.add(imageUrl)\n        cancelImageReveal()\n        revealFrameRef.current = window.requestAnimationFrame(() => {\n          revealFrameRef.current = window.requestAnimationFrame(() => {\n            setLoadedPreviewImageUrl(imageUrl)\n            revealFrameRef.current = null\n          })\n        })\n      }\n    },\n    [cancelImageReveal]\n  )\n  React.useEffect(() => {\n    cancelImageReveal()\n  }, [cancelImageReveal, previewImageUrl])\n  React.useEffect(() => cancelImageReveal, [cancelImageReveal])\n  React.useEffect(() => {\n    const image = imageRef.current\n    if (!image || !previewImageUrl) return\n    if (image.complete) {\n      markImageLoaded(image, previewImageUrl)\n    }\n  }, [markImageLoaded, previewImageUrl])\n  return (\n    <div\n      className={cx(\n        \"group overflow-hidden rounded-lg border bg-background text-foreground\",\n        className\n      )}\n    >\n      <div\n        className={cx(\n          \"relative aspect-square overflow-hidden bg-muted [contain:layout_paint]\",\n          previewClassName\n        )}\n        style={\n          previewAspectRatio\n            ? { aspectRatio: String(previewAspectRatio) }\n            : undefined\n        }\n      >\n        {previewImageUrl ? (\n          // eslint-disable-next-line @next/next/no-img-element -- Preview URLs can be transient object or presigned URLs outside Next image optimization.\n          <img\n            ref={imageRef}\n            src={previewImageUrl}\n            alt=\"\"\n            draggable={false}\n            loading=\"lazy\"\n            decoding=\"async\"\n            className={cx(\n              \"absolute inset-0 block size-full object-cover transition-[opacity,filter] duration-[160ms] ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none\",\n              showLoading ? \"opacity-0 blur-sm\" : \"blur-0 opacity-100\"\n            )}\n            onLoad={(event) => {\n              markImageLoaded(event.currentTarget, previewImageUrl)\n            }}\n            onError={() => {\n              if (previewImageUrl) {\n                revealedPreviewImageUrls.delete(previewImageUrl)\n                cancelImageReveal()\n                setFailedPreviewImageUrl(previewImageUrl)\n                setLoadedPreviewImageUrl((currentUrl) =>\n                  currentUrl === previewImageUrl ? null : currentUrl\n                )\n              }\n            }}\n          />\n        ) : null}\n        {previewContent ? (\n          <div\n            className={cx(\n              \"absolute inset-0 size-full transition-[opacity,filter] duration-[160ms] ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none\",\n              showLoading ? \"opacity-0 blur-sm\" : \"blur-0 opacity-100\"\n            )}\n          >\n            {previewContent}\n          </div>\n        ) : null}\n        {showLoading ? <FileThumbnailLoadingOverlay /> : null}\n        {showFallback ? (\n          <div className=\"absolute inset-0 bg-muted\" aria-hidden=\"true\" />\n        ) : null}\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "@components/extend/file-thumbnail.tsx"
    }
  ],
  "categories": [
    "documents"
  ],
  "type": "registry:ui",
  "dependencies": [],
  "registryDependencies": []
}
