{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "empty-states",
  "title": "Empty States",
  "type": "registry:block",
  "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"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/blocks/empty-states/empty-states.ts",
      "content": "import { LitElement, css, html, nothing } from 'lit'\nimport { RotateCcw } from 'lucide'\nimport { cn } from '../../lib/utils'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport { icon } from '../../lib/icon'\n// Side-effect import: registers <uip-button> for standalone use (consumers\n// who install only this block still get a working button).\nimport '../../components/button/button'\n\nexport type EmptyStatesVariant =\n  | 'no-data'\n  | 'no-search-results'\n  | 'first-use'\n  | 'error-recovery'\n  | 'no-access'\n  | 'sync-complete'\n\ninterface CardCopy {\n  image: string\n  heading: string\n  description: string\n  primaryLabel: string\n  secondaryLabel: string\n}\n\nconst DEFAULTS: Record<EmptyStatesVariant, CardCopy> = {\n  'no-data': {\n    image: '/illustrations/empty-no-data.jpg',\n    heading: '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  },\n  'no-search-results': {\n    image: '/illustrations/empty-no-search.jpg',\n    heading: '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  },\n  'first-use': {\n    image: '/illustrations/empty-first-use.jpg',\n    heading: '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  },\n  'error-recovery': {\n    image: '/illustrations/empty-error-recovery.jpg',\n    heading: '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  },\n  'no-access': {\n    image: '/illustrations/empty-no-access.jpg',\n    heading: '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  },\n  'sync-complete': {\n    image: '/illustrations/empty-sync-complete.jpg',\n    heading: 'Everything synced',\n    description: 'All changes were uploaded a few seconds ago. We will keep syncing in the background.',\n    primaryLabel: '',\n    secondaryLabel: 'View activity',\n  },\n}\n\nconst VARIANTS: EmptyStatesVariant[] = [\n  'no-data',\n  'no-search-results',\n  'first-use',\n  'error-recovery',\n  'no-access',\n  'sync-complete',\n]\n\nconst falseConverter = { fromAttribute: (v: string | null) => v !== null && v !== 'false' }\n\n/**\n * <uip-empty-states> — the registry EmptyStates block as a web component.\n *\n * Class strings are React's EmptyStates verbatim. `heading` is React's `title`\n * (`title` is taken by HTMLElement). `onPrimary` / `onSecondary` become\n * `primary` / `secondary` CustomEvents (bubbles, composed).\n *\n * Deviation: React's `ResponsePanel` fallback (shown when the resolved art is\n * `undefined`) is unreachable — every variant has a default image, so the art\n * is only ever a string or `''`. Pass `image=\"\"` to hide it.\n */\nexport class UipEmptyStates extends LitElement {\n  static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }\n  // :host display can't be a utility class (the host has no template of its own).\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  static properties = {\n    variant: {},\n    size: {},\n    layout: { reflect: true },\n    heading: {},\n    description: {},\n    image: {},\n    imageAlt: { attribute: 'image-alt' },\n    primaryLabel: { attribute: 'primary-label' },\n    secondaryLabel: { attribute: 'secondary-label' },\n    primaryHref: { attribute: 'primary-href' },\n    secondaryHref: { attribute: 'secondary-href' },\n    showPrimary: { type: Boolean, attribute: 'show-primary', converter: falseConverter },\n    showSecondary: { type: Boolean, attribute: 'show-secondary', converter: falseConverter },\n    primaryDisabled: { type: Boolean, attribute: 'primary-disabled' },\n    secondaryDisabled: { type: Boolean, attribute: 'secondary-disabled' },\n  }\n\n  variant: EmptyStatesVariant | 'all' = 'all'\n  size: 'sm' | 'default' = 'default'\n  layout: 'grid' | 'list' = 'grid'\n  heading?: string\n  description?: string\n  image?: string\n  imageAlt = ''\n  primaryLabel?: string\n  secondaryLabel?: string\n  primaryHref?: string\n  secondaryHref?: string\n  showPrimary = true\n  showSecondary = true\n  primaryDisabled = false\n  secondaryDisabled = false\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'empty-states')\n    this.setAttribute('data-layout', this.layout)\n  }\n\n  willUpdate() {\n    this.setAttribute('data-layout', this.layout)\n  }\n\n  private emit(name: 'primary' | 'secondary') {\n    this.dispatchEvent(new CustomEvent(name, { bubbles: true, composed: true }))\n  }\n\n  private card(v: EmptyStatesVariant) {\n    const d = DEFAULTS[v]\n    const single = this.variant === v\n    const img = single && this.image !== undefined ? this.image : d.image\n    const titleText = single && this.heading !== undefined ? this.heading : d.heading\n    const descText = single && this.description !== undefined ? this.description : d.description\n    const primaryText = single && this.primaryLabel !== undefined ? this.primaryLabel : d.primaryLabel\n    const secondaryText = single && this.secondaryLabel !== undefined ? this.secondaryLabel : d.secondaryLabel\n    const sm = this.size === 'sm'\n    const actionSize = sm ? 'xs' : 'sm'\n    return html`\n      <article\n        class=${cn(\n          'border-border bg-card overflow-hidden rounded-xl border text-left',\n          sm && 'rounded-lg',\n          this.layout === 'list' && 'w-full max-w-md',\n        )}\n        data-slot=\"empty-states-card\"\n        data-variant=${v}\n        role=${v === 'error-recovery' ? 'alert' : 'status'}\n      >\n        ${img ? html`<img src=${img} alt=${this.imageAlt} class=${cn('w-full object-cover', sm ? 'aspect-[16/9]' : 'aspect-[16/10]')} />` : nothing}\n        <div class=${sm ? 'p-5' : 'p-6'}>\n          <h3 class=\"text-sm font-medium\">${titleText}</h3>\n          <p class=\"text-muted-foreground mt-1 max-w-xs text-sm\">${descText}</p>\n          ${this.showPrimary || this.showSecondary\n            ? html`<div class=${cn('flex flex-wrap items-center gap-2', sm ? 'mt-4' : 'mt-5')}>\n                ${v === 'sync-complete'\n                  ? html`${this.showSecondary\n                      ? html`<uip-button\n                          size=${actionSize}\n                          variant=\"ghost\"\n                          ?disabled=${this.secondaryDisabled}\n                          href=${this.secondaryHref ?? nothing}\n                          @click=${() => this.emit('secondary')}\n                        >\n                          ${secondaryText}\n                        </uip-button>`\n                      : nothing}`\n                  : html`\n                      ${this.showPrimary\n                        ? html`<uip-button\n                            size=${actionSize}\n                            ?disabled=${this.primaryDisabled}\n                            href=${this.primaryHref ?? nothing}\n                            @click=${() => this.emit('primary')}\n                          >\n                            ${v === 'error-recovery' ? icon(RotateCcw, 'rotate-ccw') : nothing}${primaryText}\n                          </uip-button>`\n                        : nothing}\n                      ${this.showSecondary\n                        ? html`<uip-button\n                            size=${actionSize}\n                            variant=\"ghost\"\n                            ?disabled=${this.secondaryDisabled}\n                            href=${this.secondaryHref ?? nothing}\n                            @click=${() => this.emit('secondary')}\n                          >\n                            ${secondaryText}\n                          </uip-button>`\n                        : nothing}\n                    `}\n              </div>`\n            : nothing}\n        </div>\n      </article>\n    `\n  }\n\n  render() {\n    const shown = this.variant === 'all' ? VARIANTS : [this.variant as EmptyStatesVariant]\n    return html`\n      <div\n        data-slot=\"empty-states\"\n        data-layout=${this.layout}\n        class=${cn(\n          this.layout === 'list' ? 'flex flex-col items-center gap-4' : 'grid gap-4 sm:grid-cols-2 xl:grid-cols-3',\n        )}\n      >\n        ${shown.map((v) => this.card(v))}\n      </div>\n    `\n  }\n}\n\ncustomElements.get('uip-empty-states') || customElements.define('uip-empty-states', UipEmptyStates)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-empty-states': UipEmptyStates\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/blocks/empty-states/empty-states.ts"
    }
  ],
  "dependencies": [
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/button.json",
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
