{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"skeleton","type":"registry:ui","title":"Skeleton","description":"A placeholder with the shape of the content it stands in for.","dependencies":["clsx","tailwind-merge"],"registryDependencies":["https://caliperui.site/r/styles.json"],"files":[{"path":"ui/lib/cn.ts","type":"registry:ui","target":"components/ui/lib/cn.ts","content":"import { clsx, type ClassValue } from \"clsx\";\nimport { extendTailwindMerge } from \"tailwind-merge\";\n\nconst twMerge = extendTailwindMerge({\n  extend: { theme: { radius: [\"control\", \"surface\", \"overlay\"], shadow: [\"overlay\"] } },\n});\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"},{"path":"ui/skeleton.tsx","type":"registry:ui","target":"components/ui/skeleton.tsx","content":"\"use client\";\n\nimport {\n  useLayoutEffect,\n  useRef,\n  useState,\n  type ComponentProps,\n  type ReactNode,\n} from \"react\";\nimport { cn } from \"./lib/cn\";\nimport \"./skeleton.css\";\n\nconst widths = [\"100%\", \"94%\", \"78%\", \"88%\", \"66%\", \"84%\"];\n\nexport function Skeleton({\n  label = \"Loading content\",\n  lines = 3,\n  avatar = false,\n  loading = true,\n  children,\n  className,\n  ...props\n}: Omit<ComponentProps<\"div\">, \"children\"> & {\n  label?: string;\n  lines?: number;\n  avatar?: boolean;\n  loading?: boolean;\n  children?: ReactNode;\n}) {\n  const count = Math.min(6, Math.max(1, Math.round(lines)));\n  const placeholder = useRef<HTMLDivElement>(null);\n  const content = useRef<HTMLDivElement>(null);\n  const [height, setHeight] = useState<number>();\n  const hasContent = children != null;\n\n  useLayoutEffect(() => {\n    const node = (loading ? placeholder : content).current;\n    if (!hasContent || !node) return;\n    setHeight(node.getBoundingClientRect().height);\n  }, [avatar, count, hasContent, loading, children]);\n\n  const blocks = (\n    <div ref={placeholder} data-slot=\"skeleton-placeholder\" className=\"flex gap-3\">\n      {avatar && (\n        <span\n          data-slot=\"skeleton-block\"\n          className=\"size-8 shrink-0 rounded-surface\"\n          style={{ animationDelay: \"0ms\" }}\n        />\n      )}\n      <span className=\"flex min-w-0 flex-1 flex-col justify-center gap-2\">\n        {Array.from({ length: count }, (_, index) => (\n          <span\n            key={index}\n            data-slot=\"skeleton-block\"\n            className=\"block h-2.5 max-w-full\"\n            style={{\n              width: widths[(index + (avatar ? 1 : 0)) % widths.length],\n              animationDelay: `calc(${index + (avatar ? 1 : 0)} * var(--delay-stagger))`,\n            }}\n          />\n        ))}\n      </span>\n    </div>\n  );\n\n  if (!hasContent) {\n    return (\n      <div\n        role=\"status\"\n        aria-busy=\"true\"\n        aria-label={label}\n        data-slot=\"skeleton\"\n        data-loading=\"true\"\n        className={className}\n        {...props}\n      >\n        {blocks}\n      </div>\n    );\n  }\n\n  return (\n    <div\n      role=\"status\"\n      aria-busy={loading || undefined}\n      aria-label={loading ? label : undefined}\n      data-slot=\"skeleton\"\n      data-loading={loading}\n      data-has-content=\"true\"\n      className={cn(\"relative overflow-hidden\", className)}\n      style={height != null ? { height } : undefined}\n      {...props}\n    >\n      {blocks}\n      <div ref={content} data-slot=\"skeleton-content\">\n        {children}\n      </div>\n    </div>\n  );\n}\n"},{"path":"ui/skeleton.css","type":"registry:file","target":"components/ui/skeleton.css","content":"/* skeleton.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"skeleton\"][data-has-content=\"true\"] {\n    transition: height var(--duration-normal) var(--ease-settle);\n  }\n  [data-slot=\"skeleton\"][data-has-content=\"true\"] [data-slot=\"skeleton-placeholder\"],\n  [data-slot=\"skeleton\"][data-has-content=\"true\"] [data-slot=\"skeleton-content\"] {\n    position: absolute;\n    inset-inline: 0;\n    top: 0;\n    transition:\n      opacity var(--duration-normal) var(--ease-out),\n      transform var(--duration-normal) var(--ease-settle);\n  }\n  [data-slot=\"skeleton\"][data-loading=\"true\"] [data-slot=\"skeleton-content\"] {\n    opacity: 0;\n    transform: translateY(var(--motion-enter-distance));\n    pointer-events: none;\n  }\n  [data-slot=\"skeleton\"][data-loading=\"false\"] [data-slot=\"skeleton-placeholder\"] {\n    opacity: 0;\n    pointer-events: none;\n  }\n  [data-slot=\"skeleton-block\"] {\n    border-radius: var(--radius-control);\n    background: var(--muted);\n    animation: caliper-skeleton-pulse var(--duration-skeleton) var(--ease-in-out) infinite;\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"skeleton\"],\n    [data-slot=\"skeleton-placeholder\"],\n    [data-slot=\"skeleton-content\"] {\n      transition: none;\n    }\n    [data-slot=\"skeleton-block\"] {\n      animation: none;\n      transition: none;\n      transform: none;\n    }\n  }\n}\n"}],"categories":["feedback"],"meta":{"tier":"free","license":"MIT","family":"feedback","docs":"/docs/components/skeleton"}}