{
  "name": "vue-ui-image-view",
  "title": "vue-ui-image-view",
  "type": "registry:component",
  "description": "",
  "registryDependencies": [],
  "dependencies": [
    "@egoist/tailwindcss-icons",
    "@iconify-json/lucide",
    "prosekit@^0.22.0"
  ],
  "files": [
    {
      "path": "registry/src/vue/ui/image-view/image-view.vue",
      "type": "registry:component",
      "target": "components/editor/ui/image-view/image-view.vue",
      "content": "<script setup lang=\"ts\">\nimport { UploadTask } from 'prosekit/extensions/file'\nimport type { ImageAttrs } from 'prosekit/extensions/image'\nimport type { VueNodeViewProps } from 'prosekit/vue'\nimport { ResizableHandle, ResizableRoot } from 'prosekit/vue/resizable'\nimport { computed, ref, watchEffect } from 'vue'\n\nconst props = defineProps<VueNodeViewProps>()\n\nconst attrs = computed(() => props.node.value.attrs as ImageAttrs)\nconst url = computed(() => attrs.value.src || '')\nconst uploading = computed(() => url.value.startsWith('blob:'))\n\nconst aspectRatio = ref<number | undefined>()\nconst error = ref<string | undefined>()\nconst progress = ref(0)\n\nwatchEffect((onCleanup) => {\n  if (!uploading.value) return\n\n  const uploadTask = UploadTask.get<string>(url.value)\n  if (!uploadTask) return\n\n  let canceled = false\n\n  uploadTask.finished.catch((err) => {\n    if (canceled) return\n    error.value = String(err)\n  })\n  const unsubscribeProgress = uploadTask.subscribeProgress(({ loaded, total }) => {\n    if (canceled) return\n    progress.value = total ? loaded / total : 0\n  })\n\n  onCleanup(() => {\n    canceled = true\n    unsubscribeProgress()\n  })\n})\n\nfunction 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    aspectRatio.value = ratio\n  }\n  if (naturalWidth && naturalHeight && (!attrs.value.width || !attrs.value.height)) {\n    props.setAttrs({ width: naturalWidth, height: naturalHeight })\n  }\n}\n</script>\n\n<template>\n  <ResizableRoot\n    :width=\"attrs.width ?? undefined\"\n    :height=\"attrs.height ?? undefined\"\n    :aspect-ratio=\"aspectRatio\"\n    :data-selected=\"props.selected.value ? '' : 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    @resize-end=\"(event) => setAttrs(event.detail)\"\n  >\n    <img\n      v-if=\"url && !error\"\n      :src=\"url\"\n      alt=\"upload preview\"\n      class=\"h-full w-full max-w-full max-h-full object-contain\"\n      @load=\"handleImageLoad\"\n    />\n    <div v-if=\"uploading && !error\" 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    <div v-if=\"error\" 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    <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</template>\n"
    },
    {
      "path": "registry/src/vue/ui/image-view/index.ts",
      "type": "registry:component",
      "target": "components/editor/ui/image-view/index.ts",
      "content": "import type { Extension } from 'prosekit/core'\nimport { defineVueNodeView, type VueNodeViewComponent } from 'prosekit/vue'\n\nimport ImageView from './image-view.vue'\n\nexport function defineImageView(): Extension {\n  return defineVueNodeView({\n    name: 'image',\n    component: ImageView as VueNodeViewComponent,\n  })\n}\n"
    }
  ],
  "meta": {
    "hasIcons": true,
    "hidden": false,
    "story": "",
    "framework": "vue",
    "accumulatedFiles": [
      "registry/src/vue/ui/image-view/image-view.vue",
      "registry/src/vue/ui/image-view/index.ts"
    ],
    "internalDependencies": []
  },
  "css": {
    "@plugin @egoist/tailwindcss-icons": {}
  },
  "$schema": "https://ui.shadcn.com/schema/registry-item.json"
}
