{
  "name": "react-ui-image-view",
  "title": "react-ui-image-view",
  "type": "registry:component",
  "description": "",
  "registryDependencies": [],
  "dependencies": [
    "@egoist/tailwindcss-icons",
    "@iconify-json/lucide",
    "prosekit@^0.22.0"
  ],
  "files": [
    {
      "path": "registry/src/react/ui/image-view/image-view.tsx",
      "type": "registry:component",
      "target": "components/editor/ui/image-view/image-view.tsx",
      "content": "'use client'\n\nimport { UploadTask } from 'prosekit/extensions/file'\nimport type { ImageAttrs } from 'prosekit/extensions/image'\nimport type { ReactNodeViewProps } from 'prosekit/react'\nimport { ResizableHandle, ResizableRoot } from 'prosekit/react/resizable'\nimport { useEffect, useState, type SyntheticEvent } from 'react'\n\nexport default function ImageView(props: ReactNodeViewProps) {\n  const attrs = props.node.attrs as ImageAttrs\n  const url = attrs.src || ''\n  const uploading = url.startsWith('blob:')\n\n  const [aspectRatio, setAspectRatio] = useState<number | undefined>()\n  const [error, setError] = useState<string | undefined>()\n  const [progress, setProgress] = useState(0)\n\n  useEffect(() => {\n    if (!uploading) return\n\n    const uploadTask = UploadTask.get<string>(url)\n    if (!uploadTask) return\n\n    let canceled = false\n\n    uploadTask.finished.catch((error) => {\n      if (canceled) return\n      setError(String(error))\n    })\n    const unsubscribeProgress = uploadTask.subscribeProgress(({ loaded, total }) => {\n      if (canceled) return\n      setProgress(total ? loaded / total : 0)\n    })\n\n    return () => {\n      canceled = true\n      unsubscribeProgress()\n    }\n  }, [url, uploading])\n\n  const handleImageLoad = (event: SyntheticEvent) => {\n    const img = event.target as HTMLImageElement\n    const { naturalWidth, naturalHeight } = img\n    const ratio = naturalWidth / naturalHeight\n    if (ratio && Number.isFinite(ratio)) {\n      setAspectRatio(ratio)\n    }\n    if (naturalWidth && naturalHeight && (!attrs.width || !attrs.height)) {\n      props.setAttrs({ width: naturalWidth, height: naturalHeight })\n    }\n  }\n\n  return (\n    <ResizableRoot\n      width={attrs.width ?? undefined}\n      height={attrs.height ?? undefined}\n      aspectRatio={aspectRatio}\n      onResizeEnd={(event) => props.setAttrs(event.detail)}\n      data-selected={props.selected ? '' : undefined}\n      className=\"relative flex items-center justify-center box-border overflow-hidden my-2 group max-h-150 max-w-full min-h-16 min-w-16 outline-2 outline-transparent data-selected:outline-blue-500 outline-solid\"\n    >\n      {url && !error && (\n        <img\n          src={url}\n          onLoad={handleImageLoad}\n          alt=\"upload preview\"\n          className=\"h-full w-full max-w-full max-h-full object-contain\"\n        />\n      )}\n      {uploading && !error && (\n        <div className=\"absolute bottom-0 left-0 m-1 flex content-center items-center gap-2 rounded-sm bg-gray-800/60 p-1.5 text-xs text-white/80 transition\">\n          <div className=\"i-lucide-loader-circle size-4 animate-spin block\"></div>\n          <div>{Math.round(progress * 100)}%</div>\n        </div>\n      )}\n      {error && (\n        <div className=\"absolute bottom-0 left-0 right-0 top-0 flex flex-col items-center justify-center gap-4 bg-gray-200 p-2 text-sm dark:bg-gray-800 @container\">\n          <div className=\"i-lucide-image-off size-8 block\"></div>\n          <div className=\"hidden opacity-80 @xs:block\">\n            Failed to upload image\n          </div>\n        </div>\n      )}\n      <ResizableHandle\n        className=\"absolute bottom-0 right-0 rounded-sm m-1.5 p-1 transition bg-gray-900/30 active:bg-gray-800/60 hover:bg-gray-800/60 text-white/50 active:text-white/80 active:translate-x-0.5 active:translate-y-0.5 opacity-0 hover:opacity-100 group-hover:opacity-100 group-data-resizing:opacity-100\"\n        position=\"bottom-right\"\n      >\n        <div className=\"i-lucide-arrow-down-right size-4 block\"></div>\n      </ResizableHandle>\n    </ResizableRoot>\n  )\n}\n"
    },
    {
      "path": "registry/src/react/ui/image-view/index.ts",
      "type": "registry:component",
      "target": "components/editor/ui/image-view/index.ts",
      "content": "import type { Extension } from 'prosekit/core'\nimport { defineReactNodeView, type ReactNodeViewComponent } from 'prosekit/react'\n\nimport ImageView from './image-view.tsx'\n\nexport function defineImageView(): Extension {\n  return defineReactNodeView({\n    name: 'image',\n    component: ImageView satisfies ReactNodeViewComponent,\n  })\n}\n"
    }
  ],
  "meta": {
    "hasIcons": true,
    "hidden": false,
    "story": "",
    "framework": "react",
    "accumulatedFiles": [
      "registry/src/react/ui/image-view/image-view.tsx",
      "registry/src/react/ui/image-view/index.ts"
    ],
    "internalDependencies": []
  },
  "css": {
    "@plugin @egoist/tailwindcss-icons": {}
  },
  "$schema": "https://ui.shadcn.com/schema/registry-item.json"
}
