{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "skeleton",
  "type": "registry:ui",
  "title": "Skeleton",
  "description": "Bones sized to your text’s line box, lit by one shared slow sweep.",
  "categories": [
    "async"
  ],
  "dependencies": [],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json"
  ],
  "files": [
    {
      "path": "components/ui/skeleton.tsx",
      "content": "\"use client\";\nimport { createContext, useContext } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\n// Bones are the foreground at 7%, so they read on any surface in either theme.\n// The shimmer is one light passing across every bone at once: the gradient is\n// pinned to the viewport (background-attachment: fixed), so a wide block and a\n// short line are lit by the same sweep instead of each flashing on its own.\nconst CSS = `\n@keyframes stealth-skeleton-in { from { opacity: 0; } }\n@keyframes stealth-bone-shimmer { from { background-position: -100vw 0; } to { background-position: 100vw 0; } }\n@keyframes stealth-bone-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }\n.stealth-skeleton { animation: stealth-skeleton-in 240ms cubic-bezier(0.25, 1, 0.5, 1) both; }\n/* Defaults live in a layer below utilities, so any className you pass wins. */\n@layer components {\n  .stealth-bone { display: block; background-color: color-mix(in oklab, var(--fg) 7%, transparent); }\n  .stealth-block { width: 100%; height: 96px; border-radius: 8px; }\n  .stealth-circle { flex-shrink: 0; border-radius: 9999px; }\n}\n[data-animation=\"shimmer\"] .stealth-bone {\n  background-image: linear-gradient(100deg, transparent 38%, color-mix(in oklab, var(--fg) 9%, transparent) 50%, transparent 62%);\n  background-size: 100vw 100%;\n  background-repeat: no-repeat;\n  background-attachment: fixed;\n  animation: stealth-bone-shimmer 2.8s linear infinite;\n}\n[data-animation=\"pulse\"] .stealth-bone { animation: stealth-bone-pulse 2.4s ease-in-out infinite; }\n@media (prefers-reduced-motion: reduce) {\n  .stealth-skeleton .stealth-bone { animation: none !important; background-image: none !important; }\n}\n`;\n\ntype Animation = \"shimmer\" | \"pulse\" | \"static\";\nconst Ctx = createContext<{ animation: Animation } | null>(null);\n\nexport type SkeletonProps = React.ComponentProps<\"div\"> & {\n  /** A slow light sweep, a slow opacity pulse, or nothing. */\n  animation?: Animation;\n  /** What is loading, for screen readers: \"Loading invoices\". */\n  label?: string;\n};\n\n/** The loading container. Marks the region busy and sets the animation for every bone inside. */\nexport function Skeleton({ animation = \"shimmer\", label = \"Loading\", className, children, ...rest }: SkeletonProps) {\n  return (\n    <Ctx.Provider value={{ animation }}>\n      <div aria-busy=\"true\" data-animation={animation} className={cn(\"stealth-skeleton\", className)} {...rest}>\n        <style href=\"stealth-skeleton\" precedence=\"default\">\n          {CSS}\n        </style>\n        <span className=\"sr-only\">{label}</span>\n        <div aria-hidden className=\"contents\">\n          {children}\n        </div>\n      </div>\n    </Ctx.Provider>\n  );\n}\n\n/** Presets used on their own bring their own container; inside a Skeleton they don't nest another. */\nfunction Rooted({ root, children }: { root: SkeletonProps; children: React.ReactNode }) {\n  const inside = useContext(Ctx);\n  if (inside) return <div className={root.className} style={root.style}>{children}</div>;\n  return <Skeleton {...root}>{children}</Skeleton>;\n}\n\nconst len = (v: string | number | undefined) => (typeof v === \"number\" ? `${v}px` : v);\n// Deterministic, so server and client agree and reloads don't reshuffle.\nconst WIDTHS = [72, 56, 84, 64, 48, 78, 60, 90, 52, 68];\nconst pick = (i: number) => WIDTHS[i % WIDTHS.length];\n\nexport type SkeletonLineProps = Omit<React.ComponentProps<\"span\">, \"children\"> & {\n  /** Several lines, as a paragraph. The last one is shorter. */\n  lines?: number;\n  /** Width of a single line, or of the last line in a paragraph. */\n  width?: string | number;\n};\n\n/**\n * A line of text that hasn't arrived. It is exactly one line-height tall and\n * draws a bar the height of the letters, so set the same text size and leading\n * as the real text (className=\"text-[13px] leading-[18px]\") and nothing moves on swap.\n */\nexport function SkeletonLine({ lines = 1, width, className, style, ...rest }: SkeletonLineProps) {\n  const one = (w: string | undefined, key?: number) => (\n    <span key={key} className=\"flex h-[1lh] items-center\" style={{ width: w }}>\n      <span className=\"stealth-bone h-[0.72em] w-full rounded-[3px]\" />\n    </span>\n  );\n  if (lines <= 1)\n    return (\n      <span className={cn(\"block max-w-full\", className)} style={{ width: len(width), ...style }} {...rest}>\n        {one(\"100%\")}\n      </span>\n    );\n  return (\n    <span className={cn(\"flex w-full flex-col\", className)} style={style} {...rest}>\n      {Array.from({ length: lines }, (_, i) => one(i === lines - 1 ? (len(width) ?? \"62%\") : `${[100, 96, 98, 93][i % 4]}%`, i))}\n    </span>\n  );\n}\n\nexport type SkeletonCircleProps = Omit<React.ComponentProps<\"span\">, \"children\"> & {\n  /** Diameter in pixels. Match the avatar it stands in for. */\n  size?: number;\n};\n\nexport function SkeletonCircle({ size = 32, className, style, ...rest }: SkeletonCircleProps) {\n  return <span className={cn(\"stealth-bone stealth-circle\", className)} style={{ width: size, height: size, ...style }} {...rest} />;\n}\n\nexport type SkeletonBlockProps = Omit<React.ComponentProps<\"span\">, \"children\">;\n\n/** Any rectangle: an image, a chart, a button. Full width and 96px tall until your className says otherwise. */\nexport function SkeletonBlock({ className, ...rest }: SkeletonBlockProps) {\n  return <span className={cn(\"stealth-bone stealth-block\", className)} {...rest} />;\n}\n\nexport type SkeletonRowProps = SkeletonProps & {\n  /** How many rows. Widths vary row to row so it reads as a list, not a stamp. */\n  count?: number;\n  avatar?: boolean;\n  /** A second, quieter line under the name. */\n  meta?: boolean;\n  /** A short value at the end of the row: a time, an amount. */\n  trailing?: boolean;\n};\n\n/** A list row: 32px avatar, a 13px name, a 12px meta line and a trailing value. */\nexport function SkeletonRow({ count = 3, avatar = true, meta = true, trailing = true, className, ...root }: SkeletonRowProps) {\n  return (\n    <Rooted root={{ ...root, className: cn(\"flex flex-col\", className) }}>\n      {Array.from({ length: count }, (_, i) => (\n        <div key={i} className=\"flex items-center gap-3 py-2\">\n          {avatar && <SkeletonCircle size={32} />}\n          <div className=\"flex min-w-0 flex-1 flex-col\">\n            <SkeletonLine className=\"text-[13px] leading-[18px]\" width={`${pick(i) * 0.6}%`} />\n            {meta && <SkeletonLine className=\"text-[12px] leading-[18px]\" width={`${pick(i + 3) * 0.9}%`} />}\n          </div>\n          {trailing && <SkeletonLine className=\"text-[12px] leading-[18px]\" width={28 + (i % 3) * 6} />}\n        </div>\n      ))}\n    </Rooted>\n  );\n}\n\nexport type SkeletonCardProps = SkeletonProps & {\n  /** A media area on top: true for 16:9, or a width-to-height ratio such as 2 or 4 / 3. */\n  media?: boolean | number;\n  /** Lines of body text under the title. */\n  lines?: number;\n  /** An avatar, a name and an action along the bottom. */\n  footer?: boolean;\n};\n\n/** A card: media, a 15px title, body lines and a footer with an action. */\nexport function SkeletonCard({ media = true, lines = 2, footer = true, className, ...root }: SkeletonCardProps) {\n  return (\n    <Rooted root={{ ...root, className: cn(\"flex flex-col overflow-hidden rounded-xl border border-line\", className) }}>\n      {media !== false && <SkeletonBlock className=\"h-auto rounded-none\" style={{ aspectRatio: media === true ? 16 / 9 : media }} />}\n      <div className=\"flex flex-col gap-1.5 p-4\">\n        <SkeletonLine className=\"text-[15px] leading-[20px]\" width=\"58%\" />\n        {lines > 0 && <SkeletonLine className=\"text-[13px] leading-[19px]\" lines={lines} width=\"70%\" />}\n      </div>\n      {footer && (\n        <div className=\"flex items-center gap-2 border-t border-line px-4 py-3\">\n          <SkeletonCircle size={20} />\n          <SkeletonLine className=\"mr-auto text-[12px] leading-[18px]\" width=\"34%\" />\n          <SkeletonBlock className=\"h-7 w-16 rounded-md\" />\n        </div>\n      )}\n    </Rooted>\n  );\n}\n\nexport type SkeletonTableProps = SkeletonProps & {\n  rows?: number;\n  columns?: number;\n  /** Draw the header row. Real headers are usually known before the data, so pass false and render yours. */\n  header?: boolean;\n};\n\n/** A table: a mono header, then 36px rows. The first column is widest, the last is right-aligned like a number. */\nexport function SkeletonTable({ rows = 5, columns = 4, header = true, className, ...root }: SkeletonTableProps) {\n  const grid = { gridTemplateColumns: `minmax(0,1.6fr) repeat(${Math.max(columns - 1, 0)}, minmax(0,1fr))` };\n  const cell = (r: number, c: number, head: boolean) => (\n    <div key={c} className={cn(\"flex min-w-0 items-center\", c === columns - 1 && c > 0 && \"justify-end\")}>\n      <SkeletonLine\n        className={head ? \"text-[10.5px] leading-[14px]\" : \"text-[13px] leading-[18px]\"}\n        width={head ? `${38 + ((c * 17) % 24)}%` : c === columns - 1 && c > 0 ? `${40 + ((r * 13) % 22)}%` : `${pick(r * 3 + c)}%`}\n      />\n    </div>\n  );\n  return (\n    <Rooted root={{ ...root, className: cn(\"flex flex-col text-left\", className) }}>\n      {header && (\n        <div className=\"grid h-9 items-center gap-4 border-b border-line px-3\" style={grid}>\n          {Array.from({ length: columns }, (_, c) => cell(0, c, true))}\n        </div>\n      )}\n      {Array.from({ length: rows }, (_, r) => (\n        <div key={r} className=\"grid h-9 items-center gap-4 border-b border-line px-3 last:border-b-0\" style={grid}>\n          {Array.from({ length: columns }, (_, c) => cell(r, c, false))}\n        </div>\n      ))}\n    </Rooted>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/skeleton"
}
