{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "empty-state",
  "title": "Empty State",
  "type": "registry:ui",
  "description": "Centered placeholder with icon, headline, supporting text, and one or two actions. Drop into empty lists, blank dashboards, and unauthenticated views — the standard \"nothing here yet\" pattern.",
  "categories": [
    "feedback"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/empty-state/empty-state.ts",
      "content": "import { LitElement, css, html, nothing } from 'lit'\nimport { html as staticHtml, unsafeStatic } from 'lit/static-html.js'\nimport {\n  BellOff,\n  CloudOff,\n  Database,\n  FileX2,\n  FolderOpen,\n  ImageOff,\n  Inbox,\n  Search,\n  SearchX,\n  ServerCrash,\n  WifiOff,\n  type IconNode,\n} from 'lucide'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport { icon } from '../../lib/icon'\n\ninterface IconEntry {\n  node: IconNode\n  name: string\n}\n\n/**\n * Named icons for the `icon` attribute (kebab-case lucide names). React takes\n * a Lucide component; the attribute covers the common empty-state glyphs and\n * `slot=\"icon\"` accepts any other <svg>.\n */\nconst namedIcons: Record<string, IconEntry> = {\n  inbox: { node: Inbox, name: 'inbox' },\n  search: { node: Search, name: 'search' },\n  'search-x': { node: SearchX, name: 'search-x' },\n  'server-crash': { node: ServerCrash, name: 'server-crash' },\n  'file-x-2': { node: FileX2, name: 'file-x-corner' },\n  'folder-open': { node: FolderOpen, name: 'folder-open' },\n  database: { node: Database, name: 'database' },\n  'wifi-off': { node: WifiOff, name: 'wifi-off' },\n  'cloud-off': { node: CloudOff, name: 'cloud-off' },\n  'image-off': { node: ImageOff, name: 'image-off' },\n  'bell-off': { node: BellOff, name: 'bell-off' },\n}\n\n/**\n * <uip-empty-state> — the registry EmptyState as a web component.\n *\n *   <uip-empty-state icon=\"inbox\" title=\"No messages\" description=\"…\">\n *     <uip-button>New message</uip-button>\n *   </uip-empty-state>\n *\n * Class strings are React's verbatim. `icon` is a kebab-case lucide name (see\n * `namedIcons`); slot your own <svg slot=\"icon\"> for anything else. `title` /\n * `description` render the heading (h1-h6 via `heading-tag`, default h3) and\n * the paragraph; the default slot is the action row.\n *\n * `role` is the native host attribute (`status` default, like React — set\n * `role=\"alert\"` for assertive states); `aria-live` follows it. Parts: `base`\n * (the container), `icon`, `title`, `description`.\n */\nexport class UipEmptyState extends LitElement {\n  // React's root is a block <div>.\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  static properties = {\n    icon: { reflect: true },\n    title: {},\n    description: {},\n    headingTag: { attribute: 'heading-tag', reflect: true },\n    hasSlottedIcon: { state: true },\n  }\n\n  icon?: string\n  override title: string = ''\n  description?: string\n  headingTag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' = 'h3'\n  private hasSlottedIcon = 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-state')\n  }\n\n  protected updated() {\n    // icon() takes no extra attributes; expose the named icon as `part=\"icon\"`.\n    const svg = this.renderRoot.querySelector(':scope > div > svg')\n    if (svg && !svg.hasAttribute('part')) {\n      svg.setAttribute('part', 'icon')\n      svg.setAttribute('aria-hidden', 'true')\n    }\n  }\n\n  private onIconSlotChange(e: Event) {\n    this.hasSlottedIcon = (e.target as HTMLSlotElement)\n      .assignedNodes()\n      .some((n) => n.nodeType === 1 || n.textContent?.trim())\n  }\n\n  render() {\n    // The host's native `role` attribute is the API (never a `role` property —\n    // it already exists on HTMLElement); default to React's `status`.\n    const role = this.getAttribute('role') ?? 'status'\n    const entry = this.icon ? namedIcons[this.icon] : undefined\n    const showIcon =\n      entry && !this.hasSlottedIcon ? icon(entry.node, entry.name, 'text-muted-foreground mx-auto mb-3 size-10') : nothing\n    const tag = unsafeStatic(this.headingTag)\n    return html`<div\n      part=\"base\"\n      role=${role}\n      aria-live=${role === 'alert' ? 'assertive' : 'polite'}\n      data-uipkge=\"\"\n      data-slot=\"empty-state\"\n      class=\"flex flex-col items-center py-4 text-center\"\n    >\n      ${showIcon}\n      <slot\n        name=\"icon\"\n        class=\"[&::slotted(svg)]:text-muted-foreground [&::slotted(svg)]:mx-auto [&::slotted(svg)]:mb-3 [&::slotted(svg)]:size-10\"\n        @slotchange=${this.onIconSlotChange}\n      ></slot>\n      ${this.title\n        ? staticHtml`<${tag} part=\"title\" data-uipkge=\"\" data-slot=\"empty-state-title\" class=\"text-foreground font-medium\">${this.title}</${tag}>`\n        : nothing}\n      ${this.description\n        ? html`<p part=\"description\" data-uipkge=\"\" data-slot=\"empty-state-description\" class=\"text-muted-foreground mt-1 max-w-sm text-sm\">\n            ${this.description}\n          </p>`\n        : nothing}\n      <slot></slot>\n    </div>`\n  }\n}\n\ncustomElements.get('uip-empty-state') || customElements.define('uip-empty-state', UipEmptyState)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-empty-state': UipEmptyState\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/empty-state/empty-state.ts"
    }
  ],
  "dependencies": [
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
