{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "empty-states",
  "title": "Empty States",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/empty-states/empty-states.component.ts",
      "content": "// Gallery of six zero-data cards: no data, no search results, first-use, error recovery,\n// no access, and sync complete — each with a matching HTTP response panel, title, copy, and\n// action pair. Port of the React block 1:1.\nimport { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'\nimport { RotateCcw, LucideAngularModule } from 'lucide-angular'\nimport { cn } from '@/lib/utils'\nimport { UiButtonComponent } from '@/ui/button/button.component'\n\nexport type EmptyStateVariant =\n  'no-data' | 'no-search-results' | 'first-use' | 'error-recovery' | 'no-access' | 'sync-complete'\n\ninterface EmptyStateCard {\n  variant: EmptyStateVariant\n  image: string\n  request: string\n  status: string\n  detail: string\n  tone?: 'destructive' | 'warning'\n  title: string\n  description: string\n  primaryLabel: string\n  secondaryLabel: string\n  role: 'status' | 'alert'\n  primaryOnly: boolean\n  secondaryOnly: boolean\n}\n\nconst cards: EmptyStateCard[] = [\n  {\n    variant: 'no-data',\n    image: '/illustrations/empty-no-data.jpg',\n    request: 'GET /v1/projects',\n    status: '200 OK',\n    detail: '{ \"data\": [], \"total\": 0 }',\n    title: 'Nothing here yet',\n    description: 'Your workspace is empty. Create your first project or import existing data to get started.',\n    primaryLabel: 'Create',\n    secondaryLabel: 'Import data',\n    role: 'status',\n    primaryOnly: false,\n    secondaryOnly: false,\n  },\n  {\n    variant: 'no-search-results',\n    image: '/illustrations/empty-no-search.jpg',\n    request: 'GET /search?q=northwind',\n    status: '200 OK',\n    detail: '{ \"hits\": 0 }',\n    title: 'No matches',\n    description: 'Nothing matched your current filters. Try different keywords or broaden the search.',\n    primaryLabel: 'Clear filters',\n    secondaryLabel: 'View all',\n    role: 'status',\n    primaryOnly: false,\n    secondaryOnly: false,\n  },\n  {\n    variant: 'first-use',\n    image: '/illustrations/empty-first-use.jpg',\n    request: 'GET /v1/workspace',\n    status: '200 OK',\n    detail: '{ \"onboarded\": false }',\n    title: 'Welcome to Acme',\n    description: 'This is your new dashboard. Set up your workspace and invite the team to start collaborating.',\n    primaryLabel: 'Get started',\n    secondaryLabel: 'Take a tour',\n    role: 'status',\n    primaryOnly: false,\n    secondaryOnly: false,\n  },\n  {\n    variant: 'error-recovery',\n    image: '/illustrations/empty-error-recovery.jpg',\n    request: 'GET /api/projects',\n    status: '500 Internal Server Error',\n    detail: 'error: upstream_timeout',\n    tone: 'destructive',\n    title: 'Something broke',\n    description: \"We couldn't load your data. Your work is safe — check the connection and try again.\",\n    primaryLabel: 'Retry',\n    secondaryLabel: 'Contact support',\n    role: 'alert',\n    primaryOnly: false,\n    secondaryOnly: false,\n  },\n  {\n    variant: 'no-access',\n    image: '/illustrations/empty-no-access.jpg',\n    request: 'GET /settings/billing',\n    status: '403 Forbidden',\n    detail: 'scope billing:read — missing',\n    tone: 'warning',\n    title: 'Ask your admin',\n    description:\n      \"You don't have access to this workspace yet. Request permission and we'll notify you once it's granted.\",\n    primaryLabel: 'Request access',\n    secondaryLabel: 'Switch account',\n    role: 'status',\n    primaryOnly: false,\n    secondaryOnly: false,\n  },\n  {\n    variant: 'sync-complete',\n    image: '/illustrations/empty-sync-complete.jpg',\n    request: 'POST /sync',\n    status: '200 OK',\n    detail: '{ \"synced\": 128, \"lag_ms\": 40 }',\n    title: 'Everything synced',\n    description: 'All changes were uploaded a few seconds ago. We will keep syncing in the background.',\n    primaryLabel: 'View activity',\n    secondaryLabel: 'View activity',\n    role: 'status',\n    primaryOnly: false,\n    secondaryOnly: true,\n  },\n]\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-empty-states, [ui-empty-states]',\n  standalone: true,\n  // React renders the root <div> itself: the host stays out of layout and `class` goes to the root.\n  host: { '[attr.class]': '\"contents\"' },\n  imports: [LucideAngularModule, UiButtonComponent],\n  template: `\n    <div data-slot=\"empty-states\" [attr.data-layout]=\"layout\" [class]=\"rootClass\">\n      @for (card of visibleCards(); track card.variant) {\n        <article\n          [class]=\"cardClass\"\n          data-slot=\"empty-states-card\"\n          [attr.data-variant]=\"card.variant\"\n          [attr.role]=\"card.role\"\n        >\n          @if (hasArt(card)) {\n            <img [src]=\"art(card)\" [alt]=\"imageAlt\" [class]=\"imgClass\" />\n          } @else if (art(card) === undefined) {\n            <div class=\"border-border bg-muted/40 border-b font-mono\" aria-hidden=\"true\">\n              <div class=\"border-border text-muted-foreground truncate border-b px-3 py-2 text-xs\">\n                {{ card.request }}\n              </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                  @if (card.tone === 'destructive') {\n                    <span class=\"text-destructive\">{{ card.status }}</span>\n                  } @else if (card.tone === 'warning') {\n                    <span class=\"text-warning\">{{ card.status }}</span>\n                  } @else {\n                    {{ card.status }}\n                  }\n                </p>\n                <p class=\"text-muted-foreground\">{{ card.detail }}</p>\n              </div>\n            </div>\n          }\n          <div [class]=\"bodyClass\">\n            <h3 class=\"text-sm font-medium\">{{ pickTitle(card) }}</h3>\n            <p class=\"text-muted-foreground mt-1 max-w-xs text-sm\">{{ pickDescription(card) }}</p>\n            @if (showPrimary || showSecondary) {\n              <div [class]=\"actionsClass\">\n                @if (showPrimary && !card.secondaryOnly) {\n                  @if (primaryHref) {\n                    <a ui-button [href]=\"primaryHref\" [size]=\"actionSize\" (click)=\"primary.emit()\">\n                      @if (card.variant === 'error-recovery') {\n                        <lucide-icon [img]=\"RotateCcw\" />\n                      }\n                      {{ pickPrimary(card) }}\n                    </a>\n                  } @else {\n                    <button ui-button [size]=\"actionSize\" [disabled]=\"primaryDisabled\" (click)=\"primary.emit()\">\n                      @if (card.variant === 'error-recovery') {\n                        <lucide-icon [img]=\"RotateCcw\" />\n                      }\n                      {{ pickPrimary(card) }}\n                    </button>\n                  }\n                }\n                @if (showSecondary && !card.primaryOnly) {\n                  @if (secondaryHref) {\n                    <a\n                      ui-button\n                      [size]=\"actionSize\"\n                      variant=\"ghost\"\n                      [href]=\"secondaryHref\"\n                      (click)=\"secondary.emit()\"\n                      >{{ pickSecondary(card) }}</a\n                    >\n                  } @else {\n                    <button\n                      ui-button\n                      [size]=\"actionSize\"\n                      variant=\"ghost\"\n                      [disabled]=\"secondaryDisabled\"\n                      (click)=\"secondary.emit()\"\n                    >\n                      {{ pickSecondary(card) }}\n                    </button>\n                  }\n                }\n              </div>\n            }\n          </div>\n        </article>\n      }\n    </div>\n  `,\n})\nexport class UiEmptyStatesComponent {\n  protected readonly RotateCcw = RotateCcw\n\n  /** `'all'` renders the full gallery; a single variant renders just that card. */\n  @Input() variant: EmptyStateVariant | 'all' = 'all'\n  @Input() size: 'sm' | 'default' = 'default'\n  @Input() layout: 'grid' | 'list' = 'grid'\n  @Input('class') className?: string\n  @Input() title?: string\n  @Input() description?: string\n  /** Replaces the response panel on a single variant. Pass `''` to hide art. */\n  @Input() image?: string\n  @Input() imageAlt = ''\n  @Input() primaryLabel?: string\n  @Input() secondaryLabel?: string\n  @Input() primaryHref?: string\n  @Input() secondaryHref?: string\n  @Input() showPrimary = true\n  @Input() showSecondary = true\n  @Input() primaryDisabled = false\n  @Input() secondaryDisabled = false\n  @Output() primary = new EventEmitter<void>()\n  @Output() secondary = new EventEmitter<void>()\n\n  protected readonly allCards = cards\n\n  visibleCards(): EmptyStateCard[] {\n    return this.variant === 'all' ? cards : cards.filter((c) => c.variant === this.variant)\n  }\n\n  private isSingle(v: EmptyStateVariant): boolean {\n    return this.variant === v\n  }\n\n  pickTitle(card: EmptyStateCard): string {\n    return this.isSingle(card.variant) && this.title !== undefined ? this.title : card.title\n  }\n\n  pickDescription(card: EmptyStateCard): string {\n    return this.isSingle(card.variant) && this.description !== undefined ? this.description : card.description\n  }\n\n  pickPrimary(card: EmptyStateCard): string {\n    return this.isSingle(card.variant) && this.primaryLabel !== undefined ? this.primaryLabel : card.primaryLabel\n  }\n\n  pickSecondary(card: EmptyStateCard): string {\n    return this.isSingle(card.variant) && this.secondaryLabel !== undefined ? this.secondaryLabel : card.secondaryLabel\n  }\n\n  art(card: EmptyStateCard): string | undefined {\n    if (this.isSingle(card.variant)) return this.image === undefined ? card.image : this.image\n    return card.image\n  }\n\n  hasArt(card: EmptyStateCard): boolean {\n    const img = this.art(card)\n    return img !== undefined && img !== ''\n  }\n\n  get rootClass(): string {\n    return cn(\n      this.layout === 'list' ? 'flex flex-col items-center gap-4' : 'grid gap-4 sm:grid-cols-2 xl:grid-cols-3',\n      this.className,\n    )\n  }\n\n  get cardClass(): string {\n    return cn(\n      'border-border bg-card overflow-hidden rounded-xl border text-left',\n      this.size === 'sm' && 'rounded-lg',\n      this.layout === 'list' && 'w-full max-w-md',\n    )\n  }\n\n  get imgClass(): string {\n    return cn('w-full object-cover', this.size === 'sm' ? 'aspect-[16/9]' : 'aspect-[16/10]')\n  }\n\n  get bodyClass(): string {\n    return this.size === 'sm' ? 'p-5' : 'p-6'\n  }\n\n  get actionsClass(): string {\n    return cn('flex flex-wrap items-center gap-2', this.size === 'sm' ? 'mt-4' : 'mt-5')\n  }\n\n  get actionSize(): 'xs' | 'sm' {\n    return this.size === 'sm' ? 'xs' : 'sm'\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/empty-states/empty-states.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/empty-states/index.ts",
      "content": "export { UiEmptyStatesComponent, type EmptyStateVariant } from './empty-states.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/empty-states/index.ts"
    }
  ],
  "dependencies": [
    "lucide-angular"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/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 HTTP response panel, title, copy, and action pair.",
  "categories": [
    "layout",
    "feedback",
    "display"
  ]
}