{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "skeleton",
  "type": "registry:ui",
  "title": "Skeleton",
  "description": "Reserve space while content is still loading.",
  "categories": [
    "feedback"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://irisui.dev/r/foundation.json",
    "https://irisui.dev/r/motion-tokens.json",
    "https://irisui.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "src/registry/components/skeleton/skeleton.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useLayoutEffect, useRef, type CSSProperties, type ReactNode } from \"react\";\nimport {\n  AnimatePresence,\n  animate,\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  type AnimationPlaybackControls,\n} from \"motion/react\";\nimport { motionTokens } from \"../lib/motion-tokens\";\nimport { cn } from \"../lib/utils\";\n\nexport interface SkeletonProps {\n  label?: string;\n  lines?: number;\n  avatar?: boolean;\n  className?: string;\n  /** Content to reveal once loading finishes. With children, the placeholder crossfades into them. */\n  children?: ReactNode;\n  /** Keeps the placeholder visible while true. Only used together with children. */\n  loading?: boolean;\n}\n\n/** Follows its content height. After `morphKey` changes, the height springs from the old size to the new one and then returns to auto, so passive reflows (a resize, a font swap) follow instantly. It clips only while moving, so focus rings stay visible at rest. */\nfunction HeightFrame({\n  className,\n  busy,\n  reduce,\n  children,\n}: {\n  className?: string;\n  busy: boolean;\n  reduce: boolean | null;\n  children: ReactNode;\n}) {\n  const frame = useRef<HTMLDivElement>(null);\n  const content = useRef<HTMLDivElement>(null);\n  const height = useMotionValue<number | \"auto\">(\"auto\");\n  const changedAt = useRef(0);\n  useLayoutEffect(() => {\n    changedAt.current = performance.now();\n  }, [busy]);\n  useEffect(() => {\n    const node = content.current;\n    if (!node || typeof ResizeObserver === \"undefined\") return;\n    let last: number | undefined;\n    let controls: AnimationPlaybackControls | undefined;\n    const settle = () => {\n      height.jump(\"auto\");\n      if (frame.current) Object.assign(frame.current.style, { overflow: \"\", height: \"auto\" });\n    };\n    const observer = new ResizeObserver(([entry]) => {\n      if (!entry) return;\n      const next = entry.borderBoxSize?.[0]?.blockSize ?? node.offsetHeight;\n      const current = height.get();\n      const from = typeof current === \"number\" ? current : last;\n      last = next;\n      controls?.stop();\n      if (reduce || from === undefined || from === next || performance.now() - changedAt.current > 120) return settle();\n      // Pin the old height before this frame paints, then spring to the new one.\n      if (frame.current) Object.assign(frame.current.style, { overflow: \"hidden\", height: `${from}px` });\n      controls = animate(height, [from, next], { ...motionTokens.spring.smooth, onComplete: settle });\n    });\n    observer.observe(node);\n    return () => {\n      observer.disconnect();\n      controls?.stop();\n    };\n  }, [height, reduce]);\n  return (\n    <motion.div ref={frame} className={className} aria-busy={busy} style={{ height }}>\n      <div ref={content} className=\"relative flow-root\">\n        {children}\n      </div>\n    </motion.div>\n  );\n}\n\nexport function Skeleton({\n  label = \"Loading content\",\n  lines = 3,\n  avatar = false,\n  className,\n  children,\n  loading = true,\n}: SkeletonProps) {\n  const reduce = useReducedMotion();\n  const count = Math.min(Math.max(Math.floor(lines), 1), 6);\n  // Each block pulses a beat after the one above it, so the wait reads as one calm wave.\n  const placeholder = (extra?: string) => (\n    <output className={cn(\"flex w-full min-w-0 items-start gap-4\", extra)} aria-label={label} aria-busy=\"true\">\n      {avatar && (\n        <span\n          className=\"block flex-[0_0_auto] animate-skeleton-pulse bg-[color-mix(in_oklab,var(--line-strong)_38%,var(--surface))] motion-reduce:animate-none size-control-md rounded-full\"\n          aria-hidden=\"true\"\n        />\n      )}\n      <span className=\"grid flex-1 gap-3 pt-1\" aria-hidden=\"true\">\n        {Array.from({ length: count }, (_, index) => (\n          <span\n            className=\"block flex-[0_0_auto] animate-skeleton-pulse bg-[color-mix(in_oklab,var(--line-strong)_38%,var(--surface))] motion-reduce:animate-none h-3 w-full rounded-control first:h-4 first:w-[48%] last:w-[72%]\"\n            style={{ \"--index\": index + (avatar ? 1 : 0) } as CSSProperties}\n            key={index}\n          />\n        ))}\n      </span>\n    </output>\n  );\n  if (children === undefined) return placeholder(className);\n  return (\n    <HeightFrame className={className} busy={loading} reduce={reduce}>\n      <AnimatePresence mode=\"popLayout\" initial={false}>\n        {loading ? (\n          <motion.div\n            key=\"placeholder\"\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            transition={{\n              duration: reduce ? motionTokens.duration.instant : motionTokens.duration.fast,\n              ease: [...motionTokens.ease.standard],\n            }}\n            exit={{\n              opacity: 0,\n              transition: { duration: reduce ? motionTokens.duration.instant : motionTokens.duration.fast },\n            }}\n          >\n            {placeholder()}\n          </motion.div>\n        ) : (\n          <motion.div\n            key=\"content\"\n            initial={reduce ? { opacity: 0 } : { opacity: 0, y: 4 }}\n            animate={{ opacity: 1, y: 0 }}\n            exit={{\n              opacity: 0,\n              transition: { duration: reduce ? motionTokens.duration.instant : motionTokens.duration.fast },\n            }}\n            transition={{\n              duration: reduce ? motionTokens.duration.instant : motionTokens.duration.standard,\n              ease: [...motionTokens.ease.enter],\n            }}\n          >\n            {children}\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </HeightFrame>\n  );\n}\n\nexport default Skeleton;\n",
      "type": "registry:component",
      "target": "@components/iris/skeleton/skeleton.tsx"
    }
  ],
  "docs": "Docs and live preview: https://irisui.dev/components/skeleton",
  "meta": {
    "kind": "component",
    "group": "Feedback",
    "export": "Skeleton",
    "tags": [
      "loading",
      "status"
    ],
    "docs": "https://irisui.dev/components/skeleton"
  }
}
