{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"skeleton","type":"registry:component","title":"Skeleton","description":"Loading placeholder with one shimmer shared across the page. Wrap real content and it takes the same shape, then crossfades away.","author":"Ryan","dependencies":["clsx","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/skeleton.tsx","type":"registry:component","target":"@components/motion/skeleton.tsx","content":"\"use client\";\n// easeui.dev/components/motion/skeleton\n\nimport { type ComponentProps, type ReactNode, useEffect, useRef } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n/** A soft light band. Fixed to the viewport, so every skeleton on the page shimmers as one. */\nconst BAND =\n  \"linear-gradient(90deg, transparent, color-mix(in oklch, var(--foreground) 7%, transparent), transparent)\";\n\nconst SWEEP: Keyframe[] = [{ backgroundPosition: \"-50vw 0\" }, { backgroundPosition: \"150vw 0\" }];\nconst SWEEP_MS = 1800;\n\nexport interface SkeletonProps extends Omit<ComponentProps<\"div\">, \"ref\" | \"children\"> {\n  /** Shows the placeholder. With children, false crossfades to the real content. Default true. */\n  loading?: boolean;\n  /** Real content. It keeps its space while loading, so nothing shifts when it appears. */\n  children?: ReactNode;\n}\n\nfunction useShimmer(active: boolean) {\n  const ref = useRef<HTMLSpanElement>(null);\n\n  useEffect(() => {\n    const element = ref.current;\n    if (!active || !element || typeof element.animate !== \"function\") return;\n    const reducedMotion = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    const animation = element.animate(SWEEP, {\n      duration: SWEEP_MS,\n      iterations: Number.POSITIVE_INFINITY,\n      easing: \"ease-in-out\",\n    });\n    // Starting every skeleton at the same moment keeps their bands lined up.\n    animation.startTime = 0;\n    const sync = () => (reducedMotion.matches ? animation.pause() : animation.play());\n    sync();\n    reducedMotion.addEventListener(\"change\", sync);\n    return () => {\n      reducedMotion.removeEventListener(\"change\", sync);\n      animation.cancel();\n    };\n  }, [active]);\n\n  return ref;\n}\n\n/**\n * A loading placeholder with a quiet shimmer. Size it with classes, or wrap\n * the real content so the placeholder takes its exact shape and fades away\n * when loading ends.\n */\nexport function Skeleton({ loading = true, children, className, ...props }: SkeletonProps) {\n  const shimmer = useShimmer(loading);\n\n  const placeholder = (\n    <span\n      ref={shimmer}\n      aria-hidden=\"true\"\n      className={cn(\n        \"block rounded-lg bg-muted transition-opacity duration-200 ease-out motion-reduce:transition-none\",\n        children ? \"absolute inset-0\" : \"h-4 w-full\",\n        !loading && \"opacity-0\",\n        !children && className,\n      )}\n      style={{\n        backgroundImage: BAND,\n        backgroundSize: \"50vw 100%\",\n        backgroundRepeat: \"no-repeat\",\n        backgroundAttachment: \"fixed\",\n      }}\n    />\n  );\n\n  if (!children) return placeholder;\n\n  return (\n    <div\n      aria-busy={loading}\n      className={cn(\"relative\", className)}\n      {...props}\n    >\n      <div\n        aria-hidden={loading}\n        inert={loading}\n        className={cn(\n          \"transition-opacity duration-200 ease-out motion-reduce:transition-none\",\n          loading ? \"opacity-0\" : \"opacity-100\",\n        )}\n      >\n        {children}\n      </div>\n      {placeholder}\n    </div>\n  );\n}\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}