{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "skeleton",
  "title": "Skeleton",
  "description": "Loading placeholders with one page-wide shimmer: every skeleton shares a viewport-space highlight and one phase, so a single band sweeps across all of them. Wrap the real content and it hands off in place, fading in from a soft blur.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://eased.dev/r/eased-tokens.json",
    "https://eased.dev/r/eased-motion.json"
  ],
  "files": [
    {
      "path": "src/registry/eased/skeleton/skeleton.tsx",
      "content": "\"use client\";\n\nimport { AnimatePresence, motion, type TargetAndTransition } from \"motion/react\";\nimport { useLayoutEffect, useRef, type ReactNode } from \"react\";\nimport { enter, exit, reducedFade, useSafeReducedMotion } from \"../lib/motion\";\n\nexport type SkeletonProps = {\n  /** While true the shimmer shows; set it false and the shimmer hands off to `children`. Default true. */\n  loading?: boolean;\n  /**\n   * The real content. While loading it is laid out but invisible (and inert), so the\n   * placeholder is exactly its size and nothing shifts at the handoff. Pass stand-in text\n   * of a typical length while the data is missing, e.g. `{user?.name ?? \"Maya Okafor\"}`.\n   * Without children the skeleton is a plain block: size it with `className`.\n   */\n  children?: ReactNode;\n  /** Size and radius classes. Default radius `rounded-control-xs`; pass `rounded-pill` for avatars. */\n  className?: string;\n  /** Lay the wrapper out as a block (`grid`) instead of inline, for cards and rows. Default false. */\n  block?: boolean;\n  /**\n   * Wrapping text: the bar covers the middle 60% of each line box (roughly the x-height plus\n   * ascenders) instead of the full line height, so it reads as a line of text. Default false.\n   */\n  text?: boolean;\n};\n\n/*\n * One shimmer for the whole page: the highlight is painted in viewport space\n * (background-attachment: fixed) and every skeleton's animation is pinned to the same start\n * time, so a single band sweeps across all of them in sync instead of each one flashing on\n * its own beat. The highlight is the base tone lifted 6% in lightness, in both themes.\n */\nconst CSS = `\n@keyframes eased-shimmer { from { background-position: -170vw 0; } 70%, to { background-position: -30vw 0; } }\n.eased-skeleton {\n  background-color: var(--eased-surface-hover);\n  background-image: linear-gradient(90deg, transparent 46%, oklch(from var(--eased-surface-hover) calc(l + 0.06) c h) 50%, transparent 54%);\n  background-size: 300vw 100%;\n  background-attachment: fixed;\n  background-repeat: no-repeat;\n  animation: eased-shimmer 2000ms linear infinite;\n}\n@media (prefers-reduced-motion: reduce) { .eased-skeleton { animation: none; background-image: none; } }\n`;\n\n/** Pins every running shimmer to the document timeline's zero, so they share one phase. */\nfunction useSharedPhase<T extends HTMLElement>() {\n  const ref = useRef<T>(null);\n  useLayoutEffect(() => {\n    for (const a of ref.current?.getAnimations() ?? []) a.startTime = 0;\n  }, []);\n  return ref;\n}\n\n/**\n * A loading placeholder with one page-wide shimmer. Wrap the real content and flip\n * `loading` off: the shimmer fades out as the content fades in from a soft blur, in place.\n */\nexport function Skeleton({ loading = true, children, className = \"\", block = false, text = false }: SkeletonProps) {\n  const reduced = useSafeReducedMotion();\n  const radius = /\\brounded-/.test(className) ? \"\" : \"rounded-control-xs\";\n\n  if (children === undefined) {\n    return (\n      <AnimatePresence initial={false}>\n        {loading && (\n          <Block\n            key=\"block\"\n            className={`block ${radius} ${className}`}\n            exit={{ opacity: 0, transition: reduced ? reducedFade : exit }}\n          />\n        )}\n      </AnimatePresence>\n    );\n  }\n\n  return (\n    <span aria-busy={loading || undefined} className={`relative ${block ? \"grid\" : \"inline-grid\"} max-w-full ${className}`}>\n      <motion.span\n        inert={loading}\n        className=\"col-start-1 row-start-1 min-w-0\"\n        initial={false}\n        animate={\n          loading\n            ? { opacity: 0, filter: reduced ? \"blur(0px)\" : \"blur(4px)\" }\n            : { opacity: 1, filter: \"blur(0px)\", transitionEnd: { filter: \"none\" } }\n        }\n        transition={reduced ? reducedFade : loading ? { duration: 0 } : enter}\n      >\n        {children}\n      </motion.span>\n      <AnimatePresence initial={false}>\n        {loading && (\n          <Block\n            key=\"block\"\n            className={`absolute ${text ? \"inset-x-0 inset-y-[0.2lh]\" : \"inset-0\"} ${radius} ${className.match(/\\brounded-\\S+/g)?.join(\" \") ?? \"\"}`}\n            exit={{ opacity: 0, transition: reduced ? reducedFade : exit }}\n          />\n        )}\n      </AnimatePresence>\n    </span>\n  );\n}\n\nfunction Block({ className, exit: exitTo }: { className: string; exit: TargetAndTransition }) {\n  const ref = useSharedPhase<HTMLSpanElement>();\n  return (\n    <motion.span ref={ref} aria-hidden className={`eased-skeleton ${className}`} exit={exitTo}>\n      <style href=\"eased-skeleton\" precedence=\"default\">\n        {CSS}\n      </style>\n    </motion.span>\n  );\n}\n\nexport type SkeletonTextProps = {\n  /** Number of lines. Default 3; the last one is shorter, like a paragraph's. */\n  lines?: number;\n  loading?: boolean;\n  className?: string;\n};\n\n/** Lines sized for 13–14px text: 12px bars on a 20px rhythm. */\nexport function SkeletonText({ lines = 3, loading = true, className = \"\" }: SkeletonTextProps) {\n  return (\n    <span aria-hidden className={`flex flex-col gap-2 py-1 ${className}`}>\n      {Array.from({ length: lines }, (_, i) => (\n        <Skeleton key={i} loading={loading} className={`h-3 ${i === lines - 1 && lines > 1 ? \"w-3/5\" : \"w-full\"}`} />\n      ))}\n    </span>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@components/eased/skeleton/skeleton.tsx"
    }
  ],
  "docs": "Import Skeleton and SkeletonText from components/eased/skeleton/skeleton. The Eased tokens were merged into your global CSS; nothing to import. Dark mode follows .dark or data-theme=\"dark\".",
  "type": "registry:ui"
}