{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "skeleton-develop",
  "title": "Skeleton Develop",
  "description": "Loading wrapper whose placeholder blocks develop into the real content: a masked sweep resolves the text instead of swapping it.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/skeleton-develop/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState, type CSSProperties, type ReactNode } from \"react\";\n\n// Loading wrapper whose placeholder DEVELOPS into the real content, like a\n// print coming up in a tray: on `loading` -> false both layers are stacked and\n// a soft-edged alpha mask sweeps downward — the ghost blocks dissolve and\n// settle upward as the children un-blur behind them, top first. No swap, no\n// flash. While loading, each block carries a token-derived sweep (a recessed\n// --background trough followed by a --ns-muted crest over a --border bed), not the\n// white-to-grey shine every skeleton on the internet ships.\n//\n// Layout comes from the declarative `blocks` prop — the children are never\n// measured, because while loading they are not mounted at all. Everything\n// animating is CSS (compositor-friendly, zero rAF); React only runs the\n// three-state phase machine. prefers-reduced-motion drops to an instant swap\n// with still blocks.\n\nexport type SkeletonBlock =\n  /** One emphasised bar. `width` is a fraction of the container, 0..1. */\n  | { kind: \"heading\"; width?: number }\n  /** `lines` body bars; the last one is short, the way a paragraph ends. */\n  | { kind: \"text\"; lines?: number; width?: number }\n  /** Avatar / icon disc. `size` in px. */\n  | { kind: \"circle\"; size?: number }\n  /** Media or card placeholder. `height` in px, `width` a fraction. */\n  | { kind: \"box\"; height?: number; width?: number }\n  /** Horizontal group — an avatar beside its two lines, a row of chips. */\n  | { kind: \"row\"; gap?: number; align?: \"start\" | \"center\"; items: SkeletonBlock[] };\n\nexport interface DevelopSkeletonProps {\n  /** While true the placeholder is shown; flipping it false runs the develop. */\n  loading: boolean;\n  /** The real content. Not rendered — and so never measured — while loading. */\n  children: ReactNode;\n  /** Placeholder layout. Defaults to a heading over three lines of text. */\n  blocks?: SkeletonBlock[];\n  /** Duration of the develop transition, ms. */\n  developMs?: number;\n  /** Announced to screen readers while loading. */\n  loadingLabel?: string;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\ntype Phase = \"loading\" | \"developing\" | \"ready\";\n\nconst DEFAULT_BLOCKS: SkeletonBlock[] = [\n  { kind: \"heading\", width: 0.55 },\n  { kind: \"text\", lines: 3 },\n];\n\nconst CSS = `\n.ns-dev-bar{position:relative;overflow:hidden;background:var(--border)}\n.ns-dev-bar::after{\n  content:\"\";position:absolute;inset:0;\n  background-image:linear-gradient(100deg,\n    transparent 22%,\n    color-mix(in oklab, var(--background) 55%, transparent) 39%,\n    color-mix(in oklab, var(--ns-muted) 24%, transparent) 52%,\n    transparent 74%);\n  transform:translateX(-100%);\n  /* the delay is a custom property, not animation-delay on the host element:\n     animation-delay does not inherit into ::after, custom properties do */\n  animation:ns-dev-sweep 2600ms cubic-bezier(.42,0,.2,1) var(--ns-dev-delay,0ms) infinite;\n}\n@keyframes ns-dev-sweep{0%{transform:translateX(-100%)}64%,100%{transform:translateX(100%)}}\n\n/* Alpha masks only — the gradients below are opacity ramps, not ink. */\n.ns-dev-content{\n  -webkit-mask-image:linear-gradient(to bottom,rgb(0 0 0/1) 0 33%,rgb(0 0 0/0) 47% 100%);\n  mask-image:linear-gradient(to bottom,rgb(0 0 0/1) 0 33%,rgb(0 0 0/0) 47% 100%);\n  -webkit-mask-size:100% 300%;mask-size:100% 300%;\n  -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;\n  animation:ns-dev-resolve var(--ns-dev-ms) cubic-bezier(.22,1,.36,1) both;\n}\n@keyframes ns-dev-resolve{\n  from{-webkit-mask-position:0 100%;mask-position:0 100%;filter:blur(7px) contrast(.7)}\n  to{-webkit-mask-position:0 0%;mask-position:0 0%;filter:blur(0) contrast(1)}\n}\n.ns-dev-ghost{\n  transform-origin:top center;\n  -webkit-mask-image:linear-gradient(to bottom,rgb(0 0 0/0) 0 33%,rgb(0 0 0/1) 47% 100%);\n  mask-image:linear-gradient(to bottom,rgb(0 0 0/0) 0 33%,rgb(0 0 0/1) 47% 100%);\n  -webkit-mask-size:100% 300%;mask-size:100% 300%;\n  -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;\n  animation:ns-dev-dissolve var(--ns-dev-ms) cubic-bezier(.22,1,.36,1) both;\n}\n@keyframes ns-dev-dissolve{\n  from{-webkit-mask-position:0 100%;mask-position:0 100%;filter:blur(0);transform:scaleY(1)}\n  to{-webkit-mask-position:0 0%;mask-position:0 0%;filter:blur(5px);transform:scaleY(.975)}\n}\n@media (prefers-reduced-motion: reduce){\n  .ns-dev-bar::after{animation:none;background-image:none}\n  .ns-dev-content,.ns-dev-ghost{animation:none;-webkit-mask-image:none;mask-image:none;filter:none}\n}\n`;\n\nfunction renderBlocks(blocks: SkeletonBlock[], seq: { n: number }): ReactNode[] {\n  return blocks.map((block, i) => {\n    // negative delays offset each bar's sweep so the panel reads as one wave\n    // travelling through it rather than every bar flashing in lockstep\n    const bar = (width: number, height: number, radius: string) => (\n      <div\n        className={`ns-dev-bar ${radius}`}\n        style={\n          {\n            width: `${width * 100}%`,\n            height,\n            \"--ns-dev-delay\": `${-140 * seq.n++}ms`,\n          } as CSSProperties\n        }\n      />\n    );\n\n    switch (block.kind) {\n      case \"heading\":\n        return <div key={i}>{bar(block.width ?? 0.55, 20, \"rounded-sm\")}</div>;\n      case \"text\": {\n        const lines = Math.max(1, block.lines ?? 3);\n        const width = block.width ?? 1;\n        return (\n          <div key={i} className=\"flex flex-col gap-3\">\n            {Array.from({ length: lines }, (_, l) => (\n              // a paragraph's last line stops short — without it the block\n              // reads as a table, not prose\n              <div key={l}>{bar(l === lines - 1 ? width * 0.62 : width, 12, \"rounded-full\")}</div>\n            ))}\n          </div>\n        );\n      }\n      case \"circle\": {\n        const size = block.size ?? 40;\n        return (\n          <div\n            key={i}\n            className=\"ns-dev-bar shrink-0 rounded-full\"\n            style={\n              { width: size, height: size, \"--ns-dev-delay\": `${-140 * seq.n++}ms` } as CSSProperties\n            }\n          />\n        );\n      }\n      case \"box\":\n        return <div key={i}>{bar(block.width ?? 1, block.height ?? 120, \"rounded-md\")}</div>;\n      case \"row\":\n        return (\n          <div\n            key={i}\n            className=\"flex w-full\"\n            style={{ gap: block.gap ?? 12, alignItems: block.align === \"center\" ? \"center\" : \"flex-start\" }}\n          >\n            {block.items.map((item, j) => (\n              // a disc keeps its own size; everything else shares the rest\n              <div key={j} className={item.kind === \"circle\" ? \"shrink-0\" : \"min-w-0 flex-1\"}>\n                {renderBlocks([item], seq)}\n              </div>\n            ))}\n          </div>\n        );\n    }\n  });\n}\n\nexport function DevelopSkeleton({\n  loading,\n  children,\n  blocks = DEFAULT_BLOCKS,\n  developMs = 900,\n  loadingLabel = \"Loading\",\n  className = \"\",\n}: DevelopSkeletonProps) {\n  const [reduced, setReduced] = useState(false);\n  const [phase, setPhase] = useState<Phase>(loading ? \"loading\" : \"ready\");\n  // content that mounted already-loaded must not play a develop it never earned\n  const wasLoading = useRef(loading);\n\n  useEffect(() => {\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    const onChange = () => setReduced(mq.matches);\n    onChange();\n    mq.addEventListener(\"change\", onChange);\n    return () => mq.removeEventListener(\"change\", onChange);\n  }, []);\n\n  useEffect(() => {\n    if (loading) {\n      wasLoading.current = true;\n      setPhase(\"loading\");\n      return;\n    }\n    if (!wasLoading.current) {\n      setPhase(\"ready\");\n      return;\n    }\n    wasLoading.current = false;\n    if (reduced) {\n      setPhase(\"ready\");\n      return;\n    }\n    setPhase(\"developing\");\n    const id = window.setTimeout(() => setPhase(\"ready\"), developMs);\n    // flipping back to loading mid-develop lands here first: the pending\n    // settle is dropped, so the ghost cannot un-hide a half-developed layer\n    return () => window.clearTimeout(id);\n  }, [loading, developMs, reduced]);\n\n  const developing = phase === \"developing\";\n  const vars = { \"--ns-dev-ms\": `${developMs}ms` } as CSSProperties;\n\n  return (\n    <div className={`relative ${className}`} style={vars} aria-busy={phase !== \"ready\"}>\n      <style>{CSS}</style>\n\n      {phase !== \"loading\" && (\n        <div className={developing ? \"ns-dev-content\" : undefined}>{children}</div>\n      )}\n\n      {phase !== \"ready\" && (\n        <div\n          aria-hidden\n          className={[\n            \"flex w-full flex-col gap-4\",\n            developing ? \"ns-dev-ghost pointer-events-none absolute inset-0\" : \"\",\n          ].join(\" \")}\n        >\n          {renderBlocks(blocks, { n: 0 })}\n        </div>\n      )}\n\n      {phase === \"loading\" && (\n        <span role=\"status\" className=\"sr-only\">\n          {loadingLabel}\n        </span>\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/skeleton-develop.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)"
    },
    "light": {
      "ns-muted": "#4d4d4d"
    },
    "dark": {
      "ns-muted": "#8f8f8f"
    }
  },
  "meta": {
    "collection": "core",
    "tags": [
      "skeleton",
      "loading",
      "placeholder",
      "transition",
      "css-animation"
    ],
    "instruction": "A loading-state wrapper, not a grey rectangle: <DevelopSkeleton loading={isLoading} blocks={...}>{content}</DevelopSkeleton>. While loading it renders placeholder blocks described by a declarative `blocks` prop (heading / text with a line count / circle / box / row groups) — the children are never measured, because they are not mounted at all until the data arrives. Each bar sits on the --border token and carries a slow sweep built from a recessed --background trough followed by a --ns-muted crest, staggered bar to bar by a negative animation delay so the panel reads as one wave travelling through it, rather than the white-to-grey shine every other skeleton ships. When `loading` flips false the two layers stack and the placeholder develops into the content like a print coming up in a tray: a soft-edged alpha mask sweeps downward, the ghost blocks blur out and settle upward while the children un-blur and gain contrast behind them, top first, with no swap and no flash. Everything animating is CSS, so there is no rAF loop and no canvas; React only runs a three-state phase machine (loading / developing / ready), and flipping back to loading mid-develop cancels the pending settle rather than stranding a half-developed layer. Content that mounts already-loaded skips the transition entirely. The wrapper is aria-busy while loading with an sr-only status label; prefers-reduced-motion drops to still blocks, no sweep, and an instant swap."
  },
  "type": "registry:ui"
}