{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "empty-states",
  "title": "Empty States",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-vue/blocks/empty-states/EmptyStates.vue",
      "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from 'vue'\nimport { computed } from 'vue'\nimport { RotateCcw } from 'lucide-vue-next'\nimport { cn } from '@/lib/utils'\nimport { Button } from '@/components/ui/button'\n\nexport type EmptyStateVariant =\n  | 'no-data'\n  | 'no-search-results'\n  | 'first-use'\n  | 'error-recovery'\n  | 'no-access'\n  | 'sync-complete'\n\ninterface Props {\n  /** `'all'` renders the full gallery; a single variant renders just that card. */\n  variant?: EmptyStateVariant | 'all'\n  size?: 'sm' | 'default'\n  layout?: 'grid' | 'list'\n  class?: HTMLAttributes['class']\n  title?: string\n  description?: string\n  /** Replaces the response panel on a single variant. Pass `''` to hide art. */\n  image?: string\n  imageAlt?: string\n  primaryLabel?: string\n  secondaryLabel?: string\n  primaryHref?: string\n  secondaryHref?: string\n  showPrimary?: boolean\n  showSecondary?: boolean\n  primaryDisabled?: boolean\n  secondaryDisabled?: boolean\n  onPrimary?: () => void\n  onSecondary?: () => void\n}\n\nexport type EmptyStatesProps = Props\n\nconst props = withDefaults(defineProps<Props>(), {\n  variant: 'all',\n  size: 'default',\n  layout: 'grid',\n  imageAlt: '',\n  showPrimary: true,\n  showSecondary: true,\n  primaryDisabled: false,\n  secondaryDisabled: false,\n})\n\nconst show = (v: EmptyStateVariant) => props.variant === 'all' || props.variant === v\nconst copy = (fallback: string, override?: string) => (override === undefined ? fallback : override)\nconst forVariant = (v: EmptyStateVariant, fallback: string, override?: string) =>\n  props.variant === v ? copy(fallback, override) : fallback\n\nconst titleNoData = computed(() => forVariant('no-data', 'Nothing here yet', props.title))\nconst descNoData = computed(() =>\n  forVariant(\n    'no-data',\n    'Your workspace is empty. Create your first project or import existing data to get started.',\n    props.description,\n  ),\n)\nconst imageNoData = computed(() => forVariant('no-data', '/illustrations/empty-no-data.jpg', props.image))\nconst primaryNoData = computed(() => forVariant('no-data', 'Create', props.primaryLabel))\nconst secondaryNoData = computed(() => forVariant('no-data', 'Import data', props.secondaryLabel))\n\nconst titleSearch = computed(() => forVariant('no-search-results', 'No matches', props.title))\nconst descSearch = computed(() =>\n  forVariant(\n    'no-search-results',\n    'Nothing matched your current filters. Try different keywords or broaden the search.',\n    props.description,\n  ),\n)\nconst imageSearch = computed(() => forVariant('no-search-results', '/illustrations/empty-no-search.jpg', props.image))\nconst primarySearch = computed(() => forVariant('no-search-results', 'Clear filters', props.primaryLabel))\nconst secondarySearch = computed(() => forVariant('no-search-results', 'View all', props.secondaryLabel))\n\nconst titleFirst = computed(() => forVariant('first-use', 'Welcome to Acme', props.title))\nconst descFirst = computed(() =>\n  forVariant(\n    'first-use',\n    'This is your new dashboard. Set up your workspace and invite the team to start collaborating.',\n    props.description,\n  ),\n)\nconst imageFirst = computed(() => forVariant('first-use', '/illustrations/empty-first-use.jpg', props.image))\nconst primaryFirst = computed(() => forVariant('first-use', 'Get started', props.primaryLabel))\nconst secondaryFirst = computed(() => forVariant('first-use', 'Take a tour', props.secondaryLabel))\n\nconst titleError = computed(() => forVariant('error-recovery', 'Something broke', props.title))\nconst descError = computed(() =>\n  forVariant(\n    'error-recovery',\n    \"We couldn't load your data. Your work is safe — check the connection and try again.\",\n    props.description,\n  ),\n)\nconst imageError = computed(() => forVariant('error-recovery', '/illustrations/empty-error-recovery.jpg', props.image))\nconst primaryError = computed(() => forVariant('error-recovery', 'Retry', props.primaryLabel))\nconst secondaryError = computed(() => forVariant('error-recovery', 'Contact support', props.secondaryLabel))\n\nconst titleAccess = computed(() => forVariant('no-access', 'Ask your admin', props.title))\nconst descAccess = computed(() =>\n  forVariant(\n    'no-access',\n    \"You don't have access to this workspace yet. Request permission and we'll notify you once it's granted.\",\n    props.description,\n  ),\n)\nconst imageAccess = computed(() => forVariant('no-access', '/illustrations/empty-no-access.jpg', props.image))\nconst primaryAccess = computed(() => forVariant('no-access', 'Request access', props.primaryLabel))\nconst secondaryAccess = computed(() => forVariant('no-access', 'Switch account', props.secondaryLabel))\n\nconst titleSync = computed(() => forVariant('sync-complete', 'Everything synced', props.title))\nconst descSync = computed(() =>\n  forVariant(\n    'sync-complete',\n    'All changes were uploaded a few seconds ago. We will keep syncing in the background.',\n    props.description,\n  ),\n)\nconst imageSync = computed(() => forVariant('sync-complete', '/illustrations/empty-sync-complete.jpg', props.image))\nconst secondarySync = computed(() => forVariant('sync-complete', 'View activity', props.secondaryLabel))\n\nconst cardClass = computed(() =>\n  cn(\n    'border-border bg-card overflow-hidden rounded-xl border text-left',\n    props.size === 'sm' && 'rounded-lg',\n    props.layout === 'list' && 'w-full max-w-md',\n  ),\n)\nconst imgClass = computed(() => cn('w-full object-cover', props.size === 'sm' ? 'aspect-[16/9]' : 'aspect-[16/10]'))\nconst panelClass = 'border-border bg-muted/40 border-b font-mono'\nconst bodyClass = computed(() => (props.size === 'sm' ? 'p-5' : 'p-6'))\nconst titleClass = 'text-sm font-medium'\nconst descClass = 'text-muted-foreground mt-1 max-w-xs text-sm'\nconst actionsClass = computed(() => cn('flex flex-wrap items-center gap-2', props.size === 'sm' ? 'mt-4' : 'mt-5'))\nconst actionSize = computed(() => (props.size === 'sm' ? 'xs' : 'sm'))\n</script>\n\n<template>\n  <div\n    data-slot=\"empty-states\"\n    :data-layout=\"layout\"\n    :class=\"\n      cn(\n        layout === 'list' ? 'flex flex-col items-center gap-4' : 'grid gap-4 sm:grid-cols-2 xl:grid-cols-3',\n        props.class,\n      )\n    \"\n  >\n    <article\n      v-if=\"show('no-data')\"\n      :class=\"cardClass\"\n      data-slot=\"empty-states-card\"\n      data-variant=\"no-data\"\n      role=\"status\"\n    >\n      <img v-if=\"imageNoData\" :src=\"imageNoData\" :alt=\"imageAlt\" :class=\"imgClass\" />\n      <div v-else-if=\"imageNoData === undefined\" :class=\"panelClass\" aria-hidden=\"true\">\n        <div class=\"border-border text-muted-foreground truncate border-b px-3 py-2 text-xs\">GET /v1/projects</div>\n        <div class=\"space-y-1 p-3 text-xs leading-relaxed\">\n          <p><span class=\"text-muted-foreground\">HTTP/1.1</span> 200 OK</p>\n          <p class=\"text-muted-foreground\">{ \"data\": [], \"total\": 0 }</p>\n        </div>\n      </div>\n      <div :class=\"bodyClass\">\n        <h3 :class=\"titleClass\">{{ titleNoData }}</h3>\n        <p :class=\"descClass\">{{ descNoData }}</p>\n        <div v-if=\"showPrimary || showSecondary\" :class=\"actionsClass\">\n          <Button\n            v-if=\"showPrimary\"\n            :as=\"primaryHref ? 'a' : 'button'\"\n            :href=\"primaryHref\"\n            :size=\"actionSize\"\n            :disabled=\"primaryDisabled\"\n            @click=\"onPrimary?.()\"\n          >\n            {{ primaryNoData }}\n          </Button>\n          <Button\n            v-if=\"showSecondary\"\n            :as=\"secondaryHref ? 'a' : 'button'\"\n            :href=\"secondaryHref\"\n            :size=\"actionSize\"\n            variant=\"ghost\"\n            :disabled=\"secondaryDisabled\"\n            @click=\"onSecondary?.()\"\n          >\n            {{ secondaryNoData }}\n          </Button>\n        </div>\n      </div>\n    </article>\n\n    <article\n      v-if=\"show('no-search-results')\"\n      :class=\"cardClass\"\n      data-slot=\"empty-states-card\"\n      data-variant=\"no-search-results\"\n      role=\"status\"\n    >\n      <img v-if=\"imageSearch\" :src=\"imageSearch\" :alt=\"imageAlt\" :class=\"imgClass\" />\n      <div v-else-if=\"imageSearch === undefined\" :class=\"panelClass\" aria-hidden=\"true\">\n        <div class=\"border-border text-muted-foreground truncate border-b px-3 py-2 text-xs\">\n          GET /search?q=northwind\n        </div>\n        <div class=\"space-y-1 p-3 text-xs leading-relaxed\">\n          <p><span class=\"text-muted-foreground\">HTTP/1.1</span> 200 OK</p>\n          <p class=\"text-muted-foreground\">{ \"hits\": 0 }</p>\n        </div>\n      </div>\n      <div :class=\"bodyClass\">\n        <h3 :class=\"titleClass\">{{ titleSearch }}</h3>\n        <p :class=\"descClass\">{{ descSearch }}</p>\n        <div v-if=\"showPrimary || showSecondary\" :class=\"actionsClass\">\n          <Button\n            v-if=\"showPrimary\"\n            :as=\"primaryHref ? 'a' : 'button'\"\n            :href=\"primaryHref\"\n            :size=\"actionSize\"\n            :disabled=\"primaryDisabled\"\n            @click=\"onPrimary?.()\"\n          >\n            {{ primarySearch }}\n          </Button>\n          <Button\n            v-if=\"showSecondary\"\n            :as=\"secondaryHref ? 'a' : 'button'\"\n            :href=\"secondaryHref\"\n            :size=\"actionSize\"\n            variant=\"ghost\"\n            :disabled=\"secondaryDisabled\"\n            @click=\"onSecondary?.()\"\n          >\n            {{ secondarySearch }}\n          </Button>\n        </div>\n      </div>\n    </article>\n\n    <article\n      v-if=\"show('first-use')\"\n      :class=\"cardClass\"\n      data-slot=\"empty-states-card\"\n      data-variant=\"first-use\"\n      role=\"status\"\n    >\n      <img v-if=\"imageFirst\" :src=\"imageFirst\" :alt=\"imageAlt\" :class=\"imgClass\" />\n      <div v-else-if=\"imageFirst === undefined\" :class=\"panelClass\" aria-hidden=\"true\">\n        <div class=\"border-border text-muted-foreground truncate border-b px-3 py-2 text-xs\">GET /v1/workspace</div>\n        <div class=\"space-y-1 p-3 text-xs leading-relaxed\">\n          <p><span class=\"text-muted-foreground\">HTTP/1.1</span> 200 OK</p>\n          <p class=\"text-muted-foreground\">{ \"onboarded\": false }</p>\n        </div>\n      </div>\n      <div :class=\"bodyClass\">\n        <h3 :class=\"titleClass\">{{ titleFirst }}</h3>\n        <p :class=\"descClass\">{{ descFirst }}</p>\n        <div v-if=\"showPrimary || showSecondary\" :class=\"actionsClass\">\n          <Button\n            v-if=\"showPrimary\"\n            :as=\"primaryHref ? 'a' : 'button'\"\n            :href=\"primaryHref\"\n            :size=\"actionSize\"\n            :disabled=\"primaryDisabled\"\n            @click=\"onPrimary?.()\"\n          >\n            {{ primaryFirst }}\n          </Button>\n          <Button\n            v-if=\"showSecondary\"\n            :as=\"secondaryHref ? 'a' : 'button'\"\n            :href=\"secondaryHref\"\n            :size=\"actionSize\"\n            variant=\"ghost\"\n            :disabled=\"secondaryDisabled\"\n            @click=\"onSecondary?.()\"\n          >\n            {{ secondaryFirst }}\n          </Button>\n        </div>\n      </div>\n    </article>\n\n    <article\n      v-if=\"show('error-recovery')\"\n      :class=\"cardClass\"\n      data-slot=\"empty-states-card\"\n      data-variant=\"error-recovery\"\n      role=\"alert\"\n    >\n      <img v-if=\"imageError\" :src=\"imageError\" :alt=\"imageAlt\" :class=\"imgClass\" />\n      <div v-else-if=\"imageError === undefined\" :class=\"panelClass\" aria-hidden=\"true\">\n        <div class=\"border-border text-muted-foreground truncate border-b px-3 py-2 text-xs\">GET /api/projects</div>\n        <div class=\"space-y-1 p-3 text-xs leading-relaxed\">\n          <p>\n            <span class=\"text-muted-foreground\">HTTP/1.1</span>{{ ' ' }}\n            <span class=\"text-destructive\">500 Internal Server Error</span>\n          </p>\n          <p class=\"text-muted-foreground\">error: upstream_timeout</p>\n        </div>\n      </div>\n      <div :class=\"bodyClass\">\n        <h3 :class=\"titleClass\">{{ titleError }}</h3>\n        <p :class=\"descClass\">{{ descError }}</p>\n        <div v-if=\"showPrimary || showSecondary\" :class=\"actionsClass\">\n          <Button\n            v-if=\"showPrimary\"\n            :as=\"primaryHref ? 'a' : 'button'\"\n            :href=\"primaryHref\"\n            :size=\"actionSize\"\n            :disabled=\"primaryDisabled\"\n            @click=\"onPrimary?.()\"\n          >\n            <RotateCcw />\n            {{ primaryError }}\n          </Button>\n          <Button\n            v-if=\"showSecondary\"\n            :as=\"secondaryHref ? 'a' : 'button'\"\n            :href=\"secondaryHref\"\n            :size=\"actionSize\"\n            variant=\"ghost\"\n            :disabled=\"secondaryDisabled\"\n            @click=\"onSecondary?.()\"\n          >\n            {{ secondaryError }}\n          </Button>\n        </div>\n      </div>\n    </article>\n\n    <article\n      v-if=\"show('no-access')\"\n      :class=\"cardClass\"\n      data-slot=\"empty-states-card\"\n      data-variant=\"no-access\"\n      role=\"status\"\n    >\n      <img v-if=\"imageAccess\" :src=\"imageAccess\" :alt=\"imageAlt\" :class=\"imgClass\" />\n      <div v-else-if=\"imageAccess === undefined\" :class=\"panelClass\" aria-hidden=\"true\">\n        <div class=\"border-border text-muted-foreground truncate border-b px-3 py-2 text-xs\">GET /settings/billing</div>\n        <div class=\"space-y-1 p-3 text-xs leading-relaxed\">\n          <p>\n            <span class=\"text-muted-foreground\">HTTP/1.1</span>{{ ' ' }}\n            <span class=\"text-warning\">403 Forbidden</span>\n          </p>\n          <p class=\"text-muted-foreground\">scope billing:read — missing</p>\n        </div>\n      </div>\n      <div :class=\"bodyClass\">\n        <h3 :class=\"titleClass\">{{ titleAccess }}</h3>\n        <p :class=\"descClass\">{{ descAccess }}</p>\n        <div v-if=\"showPrimary || showSecondary\" :class=\"actionsClass\">\n          <Button\n            v-if=\"showPrimary\"\n            :as=\"primaryHref ? 'a' : 'button'\"\n            :href=\"primaryHref\"\n            :size=\"actionSize\"\n            :disabled=\"primaryDisabled\"\n            @click=\"onPrimary?.()\"\n          >\n            {{ primaryAccess }}\n          </Button>\n          <Button\n            v-if=\"showSecondary\"\n            :as=\"secondaryHref ? 'a' : 'button'\"\n            :href=\"secondaryHref\"\n            :size=\"actionSize\"\n            variant=\"ghost\"\n            :disabled=\"secondaryDisabled\"\n            @click=\"onSecondary?.()\"\n          >\n            {{ secondaryAccess }}\n          </Button>\n        </div>\n      </div>\n    </article>\n\n    <article\n      v-if=\"show('sync-complete')\"\n      :class=\"cardClass\"\n      data-slot=\"empty-states-card\"\n      data-variant=\"sync-complete\"\n      role=\"status\"\n    >\n      <img v-if=\"imageSync\" :src=\"imageSync\" :alt=\"imageAlt\" :class=\"imgClass\" />\n      <div v-else-if=\"imageSync === undefined\" :class=\"panelClass\" aria-hidden=\"true\">\n        <div class=\"border-border text-muted-foreground truncate border-b px-3 py-2 text-xs\">POST /sync</div>\n        <div class=\"space-y-1 p-3 text-xs leading-relaxed\">\n          <p><span class=\"text-muted-foreground\">HTTP/1.1</span> 200 OK</p>\n          <p class=\"text-muted-foreground\">{ \"synced\": 128, \"lag_ms\": 40 }</p>\n        </div>\n      </div>\n      <div :class=\"bodyClass\">\n        <h3 :class=\"titleClass\">{{ titleSync }}</h3>\n        <p :class=\"descClass\">{{ descSync }}</p>\n        <div v-if=\"showPrimary || showSecondary\" :class=\"actionsClass\">\n          <Button\n            v-if=\"showSecondary\"\n            :as=\"secondaryHref ? 'a' : 'button'\"\n            :href=\"secondaryHref\"\n            :size=\"actionSize\"\n            variant=\"ghost\"\n            :disabled=\"secondaryDisabled\"\n            @click=\"onSecondary?.()\"\n          >\n            {{ secondarySync }}\n          </Button>\n        </div>\n      </div>\n    </article>\n  </div>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/EmptyStates.vue"
    }
  ],
  "dependencies": [
    "lucide-vue-next"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/vue/button.json"
  ],
  "description": "Gallery of six zero-data cards — no data, no search results, first-use, error recovery, no access, and sync complete — each with a matching isometric illustration, title, copy, and action pair. `variant` picks a single card, `size` compacts it, `layout` switches grid vs list. Override title, description, image, labels, hrefs, and callbacks. Pass `image=\"\"` to hide the art.",
  "categories": [
    "layout",
    "feedback",
    "display"
  ]
}