{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "skeleton",
  "title": "Skeleton",
  "description": "Pulsing placeholder shaped like the content that is loading, so the wait feels shorter than a spinner.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/miseui/ui/skeleton.tsx",
      "content": "import type * as React from \"react\"\nimport { useId } from \"react\"\nimport { motion, useReducedMotion } from \"motion/react\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\ntype SkeletonEffect = \"pulse\" | \"wave\" | \"gooey\"\n\ntype SkeletonProps = React.ComponentProps<\"div\"> & {\n  /** How it signals loading. Undefined is the plain pulse. */\n  effect?: SkeletonEffect\n  /** With effect=\"gooey\": how many lines of text the blocks merge into. */\n  lines?: number\n}\n\nconst DEFAULT_LINES = 3\nconst BLOCKS_PER_LINE = 4\n/** Paragraphs rarely end flush; a shorter last line is what makes the shape read as text. */\nconst LAST_LINE_WIDTH = \"62%\"\nconst WAVE_SECONDS = 1.6\nconst WAVE_REST_SECONDS = 0.35\nconst WAVE_BAND = \"linear-gradient(90deg, transparent, color-mix(in oklab, var(--mise) 32%, transparent) 50%, transparent)\"\nconst BLOCK_BREATH_SECONDS = 1.4\nconst BLOCK_STAGGER_SECONDS = 0.09\n/** Past 1, so neighbours overlap at the end of the breath and the goo reads one solid bar. */\nconst BLOCK_SCALE: [number, number] = [0.42, 1.04]\n/** Blocks join across gaps under about 5 px, while the 12 px gap between lines stays open. */\nconst GOO_BLUR = 3\nconst GOO_ALPHA_SLOPE = 18\nconst GOO_ALPHA_OFFSET = -7\n/** Opaque on purpose: the alpha threshold would erase a translucent fill. */\nconst GOO_BLOCK_CLASS = \"bg-[color-mix(in_oklab,var(--mise)_38%,var(--accent))]\"\n\nfunction WaveSkeleton({ className, ...props }: React.ComponentProps<\"div\">) {\n  const reduceMotion = useReducedMotion()\n  return (\n    <div\n      data-slot=\"skeleton\"\n      data-effect=\"wave\"\n      aria-hidden=\"true\"\n      className={cn(\"relative overflow-hidden rounded-md bg-accent\", className)}\n      {...props}\n    >\n      {/* A transform string runs on the compositor: a loading screen is exactly when the main thread is busy. */}\n      <motion.span\n        data-testid=\"skeleton-wave\"\n        data-reduced={reduceMotion ? \"true\" : \"false\"}\n        className=\"absolute inset-0 block\"\n        style={{ backgroundImage: WAVE_BAND }}\n        initial={false}\n        animate={{ transform: reduceMotion ? \"translateX(0%)\" : [\"translateX(-100%)\", \"translateX(100%)\"] }}\n        transition={\n          reduceMotion\n            ? { duration: 0 }\n            : { duration: WAVE_SECONDS, ease: \"easeInOut\", repeat: Infinity, repeatDelay: WAVE_REST_SECONDS }\n        }\n      />\n    </div>\n  )\n}\n\nfunction GooeyFilter({ id }: { id: string }) {\n  return (\n    <svg className=\"absolute size-0\">\n      <defs>\n        <filter id={id} x=\"-5%\" y=\"-20%\" width=\"110%\" height=\"140%\" colorInterpolationFilters=\"sRGB\">\n          <feGaussianBlur in=\"SourceGraphic\" stdDeviation={GOO_BLUR} result=\"blur\" />\n          <feColorMatrix\n            in=\"blur\"\n            type=\"matrix\"\n            values={`1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 ${GOO_ALPHA_SLOPE} ${GOO_ALPHA_OFFSET}`}\n          />\n        </filter>\n      </defs>\n    </svg>\n  )\n}\n\nfunction GooeyLine({ line, width, reduceMotion }: { line: number; width: string; reduceMotion: boolean }) {\n  if (reduceMotion) {\n    return <div data-testid=\"skeleton-line\" className={cn(\"h-3 rounded-full\", GOO_BLOCK_CLASS)} style={{ width }} />\n  }\n  return (\n    <div data-testid=\"skeleton-line\" className=\"flex h-3\" style={{ width }}>\n      {Array.from({ length: BLOCKS_PER_LINE }, (_, block) => (\n        <motion.span\n          key={block}\n          data-testid=\"skeleton-block\"\n          className={cn(\"h-full flex-1\", GOO_BLOCK_CLASS)}\n          initial={false}\n          animate={{ scaleX: BLOCK_SCALE }}\n          transition={{\n            duration: BLOCK_BREATH_SECONDS,\n            delay: (line + block) * BLOCK_STAGGER_SECONDS,\n            ease: \"easeInOut\",\n            repeat: Infinity,\n            repeatType: \"mirror\",\n          }}\n        />\n      ))}\n    </div>\n  )\n}\n\nfunction GooeySkeleton({ className, lines, ...props }: React.ComponentProps<\"div\"> & { lines: number }) {\n  const reduceMotion = useReducedMotion() ?? false\n  const filterId = `skeleton-goo-${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`\n  const count = Math.max(1, Math.floor(lines) || 1)\n  return (\n    <div\n      data-slot=\"skeleton\"\n      data-effect=\"gooey\"\n      data-reduced={reduceMotion ? \"true\" : \"false\"}\n      aria-hidden=\"true\"\n      className={cn(\"relative rounded-md\", className)}\n      {...props}\n    >\n      {reduceMotion ? null : <GooeyFilter id={filterId} />}\n      <div\n        data-testid=\"skeleton-field\"\n        className=\"flex flex-col gap-3\"\n        style={reduceMotion ? undefined : { filter: `url(#${filterId})` }}\n      >\n        {Array.from({ length: count }, (_, line) => (\n          <GooeyLine\n            key={line}\n            line={line}\n            width={line === count - 1 && count > 1 ? LAST_LINE_WIDTH : \"100%\"}\n            reduceMotion={reduceMotion}\n          />\n        ))}\n      </div>\n    </div>\n  )\n}\n\nfunction Skeleton({ className, effect, lines = DEFAULT_LINES, ...props }: SkeletonProps) {\n  if (effect === \"wave\") return <WaveSkeleton className={className} {...props} />\n  if (effect === \"gooey\") return <GooeySkeleton className={className} lines={lines} {...props} />\n  return (\n    <div\n      data-slot=\"skeleton\"\n      data-effect={effect}\n      aria-hidden=\"true\"\n      className={cn(\"animate-pulse rounded-md bg-accent\", effect === \"pulse\" && \"motion-reduce:animate-none\", className)}\n      {...props}\n    />\n  )\n}\n\nexport { Skeleton, type SkeletonProps, type SkeletonEffect }\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "display"
  ],
  "type": "registry:ui"
}