{
  "name": "solid-ui-image-view",
  "title": "solid-ui-image-view",
  "type": "registry:component",
  "description": "",
  "registryDependencies": [],
  "dependencies": [
    "@egoist/tailwindcss-icons",
    "@iconify-json/lucide",
    "prosekit@^0.22.0"
  ],
  "files": [
    {
      "path": "registry/src/solid/ui/image-view/image-view.tsx",
      "type": "registry:component",
      "target": "components/editor/ui/image-view/image-view.tsx",
      "content": "import { UploadTask } from 'prosekit/extensions/file'\nimport type { ImageAttrs } from 'prosekit/extensions/image'\nimport type { SolidNodeViewProps } from 'prosekit/solid'\nimport { ResizableHandle, ResizableRoot } from 'prosekit/solid/resizable'\nimport { createEffect, createSignal, onCleanup, Show, type JSX } from 'solid-js'\n\nexport default function ImageView(props: SolidNodeViewProps): JSX.Element {\n  const attrs = () => props.node.attrs as ImageAttrs\n  const url = () => attrs().src || ''\n  const uploading = () => url().startsWith('blob:')\n\n  const [aspectRatio, setAspectRatio] = createSignal<number | undefined>()\n  const [error, setError] = createSignal<string | undefined>()\n  const [progress, setProgress] = createSignal(0)\n\n  createEffect(() => {\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((err) => {\n      if (canceled) return\n      setError(String(err))\n    })\n    const unsubscribeProgress = uploadTask.subscribeProgress(({ loaded, total }) => {\n      if (canceled) return\n      setProgress(total ? loaded / total : 0)\n    })\n\n    onCleanup(() => {\n      canceled = true\n      unsubscribeProgress()\n    })\n  })\n\n  const handleImageLoad = (event: Event) => {\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      attr:data-selected={props.selected ? '' : undefined}\n      class=\"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      <Show when={url() && !error()}>\n        <img\n          src={url()}\n          onLoad={handleImageLoad}\n          alt=\"upload preview\"\n          class=\"h-full w-full max-w-full max-h-full object-contain\"\n        />\n      </Show>\n      <Show when={uploading() && !error()}>\n        <div class=\"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 class=\"i-lucide-loader-circle size-4 animate-spin block\"></div>\n          <div>{Math.round(progress() * 100)}%</div>\n        </div>\n      </Show>\n      <Show when={error()}>\n        <div class=\"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 class=\"i-lucide-image-off size-8 block\"></div>\n          <div class=\"hidden opacity-80 @xs:block\">\n            Failed to upload image\n          </div>\n        </div>\n      </Show>\n      <ResizableHandle\n        class=\"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 class=\"i-lucide-arrow-down-right size-4 block\"></div>\n      </ResizableHandle>\n    </ResizableRoot>\n  )\n}\n"
    },
    {
      "path": "registry/src/solid/ui/image-view/index.ts",
      "type": "registry:component",
      "target": "components/editor/ui/image-view/index.ts",
      "content": "import type { Extension } from 'prosekit/core'\nimport { defineSolidNodeView, type SolidNodeViewComponent } from 'prosekit/solid'\n\nimport ImageView from './image-view.tsx'\n\nexport function defineImageView(): Extension {\n  return defineSolidNodeView({\n    name: 'image',\n    component: ImageView satisfies SolidNodeViewComponent,\n  })\n}\n"
    }
  ],
  "meta": {
    "hasIcons": true,
    "hidden": false,
    "story": "",
    "framework": "solid",
    "accumulatedFiles": [
      "registry/src/solid/ui/image-view/image-view.tsx",
      "registry/src/solid/ui/image-view/index.ts"
    ],
    "internalDependencies": []
  },
  "css": {
    "@plugin @egoist/tailwindcss-icons": {}
  },
  "$schema": "https://ui.shadcn.com/schema/registry-item.json"
}
