{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "states",
  "type": "registry:ui",
  "title": "Empty and loading states",
  "description": "What a view says when it has nothing yet, is waiting or has failed. Empty offers one action, a skeleton keeps the content's exact size, and an error says what went wrong and how to retry.",
  "registryDependencies": [
    "https://littmd.vercel.app/r/litt.json",
    "https://littmd.vercel.app/r/icons.json"
  ],
  "files": [
    {
      "path": "registry/litt/ui/states.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n// @litt/states 2026.11.06. What changed: https://littmd.vercel.app/changelog.md\n\n/**\n * Empty, loading and error states (components.md, \"Empty and loading states\").\n *\n * - Empty: what will appear here, in one sentence, and one action to make it appear.\n *   Left-aligned in its container. No illustration.\n * - Skeleton: a surface-2 block at the real content's dimensions, so nothing jumps when\n *   the content arrives. Loading groups the blocks and says what is loading.\n * - ErrorState: what went wrong in plain words, then how to fix it or retry. The status\n *   color is on the icon only.\n * - Static. Nothing here moves, so there is no tier. The spec allows a skeleton to pulse in\n *   opacity for at most 240ms a pass, but no tier token gives a duration for a pulse (instant is the\n *   hover tween, the others run longer than 240ms) and a literal would break the rules, so the\n *   skeleton stays still. A state that swaps for the next one appears at once.\n * - Corners: the panel is rounded-litt-surface; skeleton blocks are rounded-litt-control (text, covers),\n *   rounded-litt-surface (a card) or rounded-litt-pill (an avatar), concentric as everywhere.\n */\nimport { useEffect, useState, type HTMLAttributes, type ReactNode } from \"react\";\nimport { AlertIcon } from \"./icons\";\n\n/** The panel Empty and ErrorState sit in: a surface-2 fill and a line hairline, no shadow. Flat: a grey (surface-3) card, no hairline. */\nconst PANEL = \"rounded-litt-surface bg-litt-surface-2 p-8 shadow-[inset_0_0_0_1px_var(--litt-line)] in-[[data-material=flat]]:bg-litt-surface-3 in-[[data-material=flat]]:shadow-none\";\n\n/* -------------------------------------------------------------------------- */\n/* Empty                                                                      */\n/* -------------------------------------------------------------------------- */\n\nexport type EmptyProps = HTMLAttributes<HTMLDivElement> & {\n  /** Optional short line that says what is empty, such as \"No projects yet.\" Leave it out when the sentence says it all. */\n  title?: ReactNode;\n  /** The one sentence: what will appear here. */\n  children: ReactNode;\n  /** The one action that makes it appear, usually a Button. A ghost button needs -ml-litt-control-pad-sm so its label lines up with the text. */\n  action?: ReactNode;\n  /** Drops the fill, hairline and padding, for a container that already draws its own box. */\n  bare?: boolean;\n  /** Extra classes for the panel. */\n  className?: string;\n};\n\n/**\n * What will appear here, and the one action that makes it appear. Left-aligned, no illustration.\n * Pass role=\"status\" when it shows up in answer to something, such as a filter, so it is announced.\n */\nexport function Empty({ title, children, action, bare = false, className = \"\", ...rest }: EmptyProps) {\n  return (\n    <div className={\"grid justify-items-start gap-6 \" + (bare ? \"\" : PANEL + \" \") + className} {...rest}>\n      <div className=\"grid max-w-(--litt-measure) gap-1\">\n        {title && <p className=\"text-litt-body font-litt-title text-litt-fg-muted\">{title}</p>}\n        <p className={\"text-pretty \" + (title ? \"text-litt-small text-litt-fg-subtle\" : \"text-litt-body text-litt-fg-muted\")}>{children}</p>\n      </div>\n      {action}\n    </div>\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/* Skeleton and Loading                                                       */\n/* -------------------------------------------------------------------------- */\n\nexport type SkeletonText = \"caption\" | \"small\" | \"body\" | \"reading\" | \"h4\" | \"h3\" | \"h2\" | \"h1\";\nexport type SkeletonRadius = \"control\" | \"cover\" | \"surface\" | \"pill\";\n\nconst TEXT: Record<SkeletonText, string> = {\n  caption: \"text-litt-caption\",\n  small: \"text-litt-small\",\n  body: \"text-litt-body\",\n  reading: \"text-litt-reading\",\n  h4: \"text-litt-h4\",\n  h3: \"text-litt-h3\",\n  h2: \"text-litt-h2\",\n  h1: \"text-litt-h1\",\n};\n\nconst RADIUS: Record<SkeletonRadius, string> = {\n  control: \"rounded-litt-control\",\n  cover: \"rounded-[calc(var(--litt-radius-surface)-4px)]\",\n  surface: \"rounded-litt-surface\",\n  pill: \"rounded-litt-pill\",\n};\n\nexport type SkeletonProps = HTMLAttributes<HTMLDivElement> & {\n  /** Sizes the block as one line of that text style: a bar about as tall as the letters, centred in the line's real height, so a row never changes height when the text arrives. Leave it out for a plain block sized by className. */\n  text?: SkeletonText;\n  /** The corners: control for text and tags, cover for a cover set 4px into a card (as Card's), surface for a card, pill for an avatar. */\n  radius?: SkeletonRadius;\n  /** Size it here, to the real content's: w-40, h-10, size-8 or aspect-[8/5]. With text set, it sets the width only. */\n  className?: string;\n};\n\n/**\n * A placeholder at the real content's dimensions, filled with surface-2. Static, and hidden from\n * screen readers: wrap blocks in Loading to say what is loading.\n */\nexport function Skeleton({ text, radius = \"control\", className = \"\", ...rest }: SkeletonProps) {\n  if (text) {\n    return (\n      <div aria-hidden=\"true\" className={`flex h-lh items-center ${TEXT[text]} ${className}`} {...rest}>\n        <div className={`h-[0.75em] w-full bg-litt-surface-2 ${RADIUS[radius]}`} />\n      </div>\n    );\n  }\n  return <div aria-hidden=\"true\" className={`bg-litt-surface-2 ${RADIUS[radius]} ${className}`} {...rest} />;\n}\n\nexport type LoadingProps = HTMLAttributes<HTMLDivElement> & {\n  /** What is loading, read to screen readers: \"Loading projects\". The skeleton blocks stay silent. */\n  label?: string;\n  /** The Skeleton blocks, laid out as the real content will be. */\n  children: ReactNode;\n  /** Extra classes for the group, such as the list's box or the grid's columns. */\n  className?: string;\n};\n\n/** Groups Skeleton blocks and announces its label once as a status. Swap it for the content when it arrives. */\nexport function Loading({ label = \"Loading\", children, className = \"\", ...rest }: LoadingProps) {\n  /* The status mounts empty and takes its label a frame later: screen readers often stay silent for a live region\n     that arrives with its words already in it. */\n  const [said, setSaid] = useState(false);\n  useEffect(() => {\n    const frame = requestAnimationFrame(() => setSaid(true));\n    return () => cancelAnimationFrame(frame);\n  }, []);\n  return (\n    <div role=\"status\" className={className} {...rest}>\n      <span className=\"sr-only\">{said ? label : \"\"}</span>\n      {children}\n    </div>\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/* Error                                                                      */\n/* -------------------------------------------------------------------------- */\n\nexport type ErrorStateProps = HTMLAttributes<HTMLDivElement> & {\n  /** What went wrong, in plain words: \"The deploy to atelier-nord.com failed.\" */\n  title: ReactNode;\n  /** The cause and how to fix it, in a sentence or two. Give this or an action, so the reader is never left with only the bad news. */\n  children?: ReactNode;\n  /** The retry or the fix, usually a Button. */\n  action?: ReactNode;\n  /** Drops the fill, hairline and padding, for a container that already draws its own box. */\n  bare?: boolean;\n  /** Extra classes for the panel. */\n  className?: string;\n};\n\n/**\n * What went wrong, then how to fix it or retry. The danger color is on the icon only, and the words\n * say it too. It is a role=\"alert\", so it is announced when it appears; pass role=\"status\" if it is not urgent.\n */\nexport function ErrorState({ title, children, action, bare = false, className = \"\", ...rest }: ErrorStateProps) {\n  return (\n    <div role=\"alert\" className={\"flex items-start gap-2 \" + (bare ? \"\" : PANEL + \" \") + className} {...rest}>\n      {/* The wrapper takes the title's line height, so the 16px icon centres on the first line. */}\n      <span className=\"grid h-lh shrink-0 place-items-center text-litt-body text-litt-danger\">\n        <AlertIcon />\n      </span>\n      <div className=\"grid min-w-0 max-w-(--litt-measure) justify-items-start gap-6\">\n        <div className=\"grid gap-1\">\n          <p className=\"text-litt-body font-litt-title text-litt-fg\">{title}</p>\n          {children && <p className=\"text-pretty text-litt-small text-litt-fg-subtle\">{children}</p>}\n        </div>\n        {action}\n      </div>\n    </div>\n  );\n}\n",
      "target": "components/litt/states.tsx"
    }
  ],
  "docs": "Import Empty, Skeleton, Loading and ErrorState from components/litt/states. Wrap Skeleton blocks in Loading, so a screen reader hears what is loading once.",
  "meta": {
    "version": "2026.11.06",
    "released": "2026-11-06",
    "changelog": "https://littmd.vercel.app/changelog.md"
  }
}
