{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "skeleton",
  "title": "Skeleton",
  "type": "registry:ui",
  "description": "Animated placeholder rectangles for loading states — drop one in shape of the content that’s about to render. Variants for text lines, avatars, rounded rectangles, and circles.",
  "categories": [
    "feedback"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/skeleton/skeleton.ts",
      "content": "import { LitElement, css, html, nothing } from 'lit'\nimport { styleMap } from 'lit/directives/style-map.js'\nimport { cn } from '../../lib/utils'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport { skeletonLoaderVariants, type SkeletonLoaderVariants } from './skeleton.variants'\n\n/*\n * Shimmer keyframes — React's `shimmerCss` verbatim (React injects it as a\n * <style> tag). A keyframe colour-mix sweep has no Tailwind utility, so this\n * is the one sanctioned exception to \"no CSS beyond :host display\".\n */\nconst shimmer = css`\n  .skeleton-shimmer {\n    background: linear-gradient(\n      90deg,\n      color-mix(in srgb, var(--muted) 100%, transparent) 0%,\n      color-mix(in srgb, var(--muted) 60%, var(--foreground) 8%) 50%,\n      color-mix(in srgb, var(--muted) 100%, transparent) 100%\n    );\n    background-size: 200% 100%;\n    animation: skeleton-shimmer 1.8s linear infinite;\n  }\n  @keyframes skeleton-shimmer {\n    from {\n      background-position: 200% 0;\n    }\n    to {\n      background-position: -200% 0;\n    }\n  }\n  @media (prefers-reduced-motion: reduce) {\n    .skeleton-shimmer {\n      animation: none;\n      background: var(--muted);\n    }\n  }\n`\n\ntype SkeletonVariant = 'rectangular' | 'rounded' | 'circular' | 'text' | 'avatar' | 'image' | 'card' | 'table-row'\n\nconst skeletonVariantClasses: Record<SkeletonVariant, string> = {\n  rectangular: '',\n  rounded: 'rounded-md',\n  circular: 'rounded-full',\n  text: 'rounded h-4 w-full',\n  avatar: 'rounded-full size-10',\n  image: 'rounded-lg size-24',\n  card: 'rounded-xl size-full min-h-[120px]',\n  'table-row': 'rounded h-10 w-full',\n}\n\n/**\n * <uip-skeleton> — the registry Skeleton.\n *\n * React puts `className` on the shimmer <div>. Here consumer classes go on the\n * host (sized/rounded by the page's own Tailwind) and the inner shimmer fills\n * it (`size-full`, plus `rounded-[inherit]` for `rectangular`); variant\n * classes still sit on the inner div, as in React. `loading=false` renders the default slot instead.\n */\nexport class UipSkeleton extends LitElement {\n  // :host display can't be a utility class (the host has no template of its own).\n  static styles = [tailwind, shimmer, css`:host { display: block; }`]\n\n  static properties = {\n    variant: { reflect: true },\n    width: {},\n    height: {},\n    loading: { type: Boolean },\n  }\n\n  variant: SkeletonVariant = 'rectangular'\n  width?: string\n  height?: string\n  loading = true\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', 'skeleton')\n  }\n\n  // Fill the host for the shape-only variants (their size comes from the\n  // consumer's classes on the host); sized variants keep React's classes.\n  private fill() {\n    const v = this.variant ?? 'rectangular'\n    if (v === 'rectangular') return 'size-full rounded-[inherit]'\n    return v === 'rounded' || v === 'circular' ? 'size-full' : ''\n  }\n\n  render() {\n    if (!this.loading) return html`<slot></slot>`\n    const style: Record<string, string> = {}\n    if (this.width) style.width = this.width\n    if (this.height) style.height = this.height\n    return html`<div\n      part=\"base\"\n      data-slot=\"skeleton\"\n      aria-hidden=\"true\"\n      class=${cn(this.fill(), 'skeleton-shimmer', skeletonVariantClasses[this.variant ?? 'rectangular'])}\n      style=${styleMap(style)}\n    ></div>`\n  }\n}\n\n/**\n * <uip-skeleton-group> — the registry SkeletonGroup (`space-y-2` wrapper).\n * Slotted children aren't shadow-tree children, so React's `space-y-2` is\n * expressed as `flex flex-col gap-2` (a `::slotted()` margin loses to the\n * host page's preflight margin reset).\n * React's `tag` prop has no equivalent (the host is the element).\n */\nexport class UipSkeletonGroup extends LitElement {\n  static styles = [tailwind, css`:host { display: block; }`]\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', 'skeleton-group')\n  }\n\n  render() {\n    return html`<div part=\"base\" data-slot=\"skeleton-group\" class=\"flex flex-col gap-2\"><slot></slot></div>`\n  }\n}\n\n/**\n * <uip-skeleton-text> — the registry SkeletonText: `lines` shimmer bars, with\n * `first-line-width` / `last-line-width` tweaks.\n */\nexport class UipSkeletonText extends LitElement {\n  static styles = [tailwind, shimmer, css`:host { display: block; }`]\n\n  static properties = {\n    lines: { type: Number },\n    lastLineWidth: { attribute: 'last-line-width' },\n    firstLineWidth: { attribute: 'first-line-width' },\n  }\n\n  lines = 3\n  lastLineWidth = '80%'\n  firstLineWidth = '100%'\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', 'skeleton-text')\n  }\n\n  render() {\n    const lineWidths = Array.from({ length: this.lines }, (_, i) => {\n      if (i === 0) return this.firstLineWidth\n      if (i === this.lines - 1) return this.lastLineWidth\n      return '100%'\n    })\n    return html`<div part=\"base\" data-slot=\"skeleton-text\" aria-hidden=\"true\" class=\"space-y-2\">\n      ${lineWidths.map((width) => html`<div class=\"skeleton-shimmer h-4 rounded\" style=${styleMap({ width })}></div>`)}\n    </div>`\n  }\n}\n\ntype LoaderVariant = NonNullable<SkeletonLoaderVariants['variant']>\n\n/**\n * <uip-skeleton-loader> — the registry SkeletonLoader: preset placeholder\n * compositions (`variant`, `rows`). `loading=false` renders the default slot;\n * `boilerplate` adds the muted overlay when not loading, as in React.\n */\nexport class UipSkeletonLoader extends LitElement {\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  static properties = {\n    variant: { reflect: true },\n    loading: { type: Boolean },\n    rows: { type: Number },\n    boilerplate: { type: Boolean },\n  }\n\n  variant: LoaderVariant = 'text'\n  loading = true\n  rows = 1\n  boilerplate = 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', 'skeleton-loader')\n  }\n\n  private body() {\n    const v = (variant: LoaderVariant, extra = '') => html`<div class=${cn(skeletonLoaderVariants({ variant }), extra)}></div>`\n    const rows = Array.from({ length: this.rows })\n    const variant = this.variant\n    if (this.rows === 1) return this.loading ? v(variant) : html`<slot></slot>`\n    if (!this.loading) return html`<slot></slot>`\n    switch (variant) {\n      case 'article':\n        return html`${v('heading', 'mb-4')}${v('text', 'mb-2')}${v('text', 'mb-2')}${v('text', 'w-3/4')}`\n      case 'card':\n        return html`${v('image-large', 'mb-4')}${v('heading-small', 'mb-2')}${v('text', 'mb-2')}${v('text', 'w-1/2')}`\n      case 'card-avatar':\n        return html`<div class=\"mb-4 flex items-center gap-4\">\n          ${v('avatar-large')}\n          <div class=\"flex-1 space-y-2\">${v('heading-small')}${v('text', 'w-1/2')}</div>\n        </div>`\n      case 'actions':\n        return html`<div class=\"flex gap-2\">${v('button')}${v('button')}</div>`\n      case 'table':\n        return rows.map(() => v('table-row', 'mb-2'))\n      case 'list-item':\n        return rows.map(\n          () => html`<div class=\"mb-2 flex items-center gap-3\">\n            ${v('avatar-small')}\n            <div class=\"flex-1\">${v('text')}</div>\n          </div>`,\n        )\n      case 'list-item-two-line':\n        return rows.map(\n          () => html`<div class=\"mb-2 flex items-center gap-3\">\n            ${v('avatar')}\n            <div class=\"flex-1 space-y-2\">${v('text')}${v('text', 'w-3/4')}</div>\n          </div>`,\n        )\n      case 'list-item-three-line':\n        return rows.map(\n          () => html`<div class=\"mb-2 flex items-start gap-3\">\n            ${v('avatar')}\n            <div class=\"flex-1 space-y-2\">${v('text')}${v('text')}${v('text', 'w-2/3')}</div>\n          </div>`,\n        )\n      default:\n        return rows.map(() => v(variant, 'mb-2'))\n    }\n  }\n\n  render() {\n    return html`<div part=\"base\" data-slot=\"skeleton-loader\" class=\"space-y-2\">\n      ${this.body()}${this.boilerplate && !this.loading ? html`<div class=\"bg-muted/50 absolute inset-0\"></div>` : nothing}\n    </div>`\n  }\n}\n\ncustomElements.get('uip-skeleton') || customElements.define('uip-skeleton', UipSkeleton)\ncustomElements.get('uip-skeleton-group') || customElements.define('uip-skeleton-group', UipSkeletonGroup)\ncustomElements.get('uip-skeleton-text') || customElements.define('uip-skeleton-text', UipSkeletonText)\ncustomElements.get('uip-skeleton-loader') || customElements.define('uip-skeleton-loader', UipSkeletonLoader)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-skeleton': UipSkeleton\n    'uip-skeleton-group': UipSkeletonGroup\n    'uip-skeleton-text': UipSkeletonText\n    'uip-skeleton-loader': UipSkeletonLoader\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/skeleton/skeleton.ts"
    },
    {
      "path": "packages/elements-lit/src/components/skeleton/skeleton.variants.ts",
      "content": "import type { VariantProps } from 'class-variance-authority'\nimport { cva } from 'class-variance-authority'\n\n/**\n * Variant definitions live in their own file (rather than inline in the\n * SFC) so `SkeletonLoader.vue` can import them without the cva runtime\n * coupling that breaks SSR when several `<SkeletonLoader>` instances\n * render before the module graph fully resolves. Sibling pattern to\n * `card/card.variants.ts`.\n */\nexport const skeletonLoaderVariants = cva('bg-muted animate-pulse rounded', {\n  variants: {\n    variant: {\n      text: 'h-4 w-full',\n      chip: 'h-8 w-24 rounded-full',\n      'chip-icon': 'h-8 w-8 rounded-full',\n      article: '',\n      avatar: 'size-12 rounded-full',\n      'avatar-small': 'size-8 rounded-full',\n      'avatar-large': 'size-16 rounded-full',\n      heading: 'h-6 w-3/4',\n      'heading-medium': 'h-5 w-1/2',\n      'heading-small': 'h-4 w-1/3',\n      image: 'aspect-video w-full rounded-lg',\n      'image-small': 'h-32 w-32 rounded-lg',\n      'image-large': 'h-64 w-full rounded-lg',\n      card: '',\n      'card-avatar': '',\n      actions: '',\n      table: 'h-4 w-full',\n      'table-row': 'h-12 w-full',\n      button: 'h-10 w-24 rounded-md',\n      'button-icon': 'h-10 w-10 rounded-md',\n      badge: 'h-6 w-16 rounded-full',\n      tab: 'h-8 w-24 rounded-md',\n      'date-picker': 'h-10 w-full',\n      'list-item': 'h-16 w-full',\n      'list-item-two-line': 'h-20 w-full',\n      'list-item-three-line': 'h-24 w-full',\n    },\n  },\n  defaultVariants: {\n    variant: 'text',\n  },\n})\n\nexport type SkeletonLoaderVariants = VariantProps<typeof skeletonLoaderVariants>\n",
      "type": "registry:file",
      "target": "src/uip/components/skeleton/skeleton.variants.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority",
    "lit"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
