{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "skeleton-sweep",
  "type": "registry:component",
  "title": "Skeleton Sweep",
  "description": "Loading placeholders lit by one sweep of light that crosses the whole card at once, with a matching glint for “Drafting…” status lines.",
  "categories": [
    "motion",
    "Loading"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/devl/skeleton-sweep.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { useReducedMotion } from \"motion/react\";\nimport {\n  type ComponentProps,\n  createContext,\n  type CSSProperties,\n  type RefObject,\n  useCallback,\n  useContext,\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ntype Entry = { block: HTMLElement; band: HTMLElement; anim?: Animation };\ntype Register = (block: HTMLElement, band: HTMLElement) => () => void;\n\nconst SweepContext = createContext<Register | null>(null);\n\nconst SWEEP_EASE = \"cubic-bezier(0.4, 0.14, 0.6, 0.86)\";\nconst PULSE: Keyframe[] = [{ opacity: 1 }, { opacity: 0.55 }, { opacity: 1 }];\n\nfunction useOnScreen(ref: RefObject<HTMLElement | null>) {\n  const [onScreen, setOnScreen] = useState(true);\n  useEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    const io = new IntersectionObserver(([entry]) => setOnScreen(entry?.isIntersecting ?? true));\n    io.observe(el);\n    return () => io.disconnect();\n  }, [ref]);\n  return onScreen;\n}\n\nexport interface SkeletonSweepProps extends ComponentProps<\"div\"> {\n  duration?: number;\n  pause?: number;\n}\n\nexport function SkeletonSweep({\n  duration = 1.2,\n  pause = 0.2,\n  className,\n  children,\n  style,\n  ...props\n}: SkeletonSweepProps) {\n  const ref = useRef<HTMLDivElement>(null);\n  const entries = useRef(new Set<Entry>());\n  const observer = useRef<ResizeObserver | null>(null);\n  const [width, setWidth] = useState(0);\n  const reduce = useReducedMotion();\n  const onScreen = useOnScreen(ref);\n  const config = useRef({ duration, pause, width, reduce, onScreen });\n  config.current = { duration, pause, width, reduce, onScreen };\n\n  const start = useCallback((entry: Entry) => {\n    entry.anim?.cancel();\n    entry.anim = undefined;\n    const { duration, pause, width, reduce, onScreen } = config.current;\n    if (!onScreen || (!reduce && !width)) return;\n    const cycle = (duration + pause) * 1000;\n    entry.anim = reduce\n      ? entry.block.animate(PULSE, { duration: 1800, easing: \"ease-in-out\", iterations: Number.POSITIVE_INFINITY })\n      : entry.band.animate(\n          [\n            { transform: `translateX(${-0.66 * width}px)`, offset: 0, easing: SWEEP_EASE },\n            { transform: `translateX(${0.66 * width}px)`, offset: duration / (duration + pause) },\n            { transform: `translateX(${0.66 * width}px)`, offset: 1 },\n          ],\n          { duration: cycle, iterations: Number.POSITIVE_INFINITY },\n        );\n    // Same start time for every block, so late arrivals join the sweep in step.\n    entry.anim.startTime = 0;\n  }, []);\n\n  const layout = useCallback(() => {\n    const group = ref.current;\n    if (!group) return;\n    const g = group.getBoundingClientRect();\n    const scale = group.offsetWidth ? g.width / group.offsetWidth : 1;\n    group.style.setProperty(\"--sweep-w\", `${group.offsetWidth}px`);\n    group.style.setProperty(\"--sweep-h\", `${group.offsetHeight}px`);\n    for (const { block } of entries.current) {\n      const r = block.getBoundingClientRect();\n      block.style.setProperty(\"--sweep-bx\", `${(r.left - g.left) / scale}px`);\n      block.style.setProperty(\"--sweep-by\", `${(r.top - g.top) / scale}px`);\n    }\n    setWidth(group.offsetWidth);\n  }, []);\n\n  useLayoutEffect(() => {\n    const ro = new ResizeObserver(layout);\n    observer.current = ro;\n    if (ref.current) ro.observe(ref.current);\n    for (const { block } of entries.current) ro.observe(block);\n    layout();\n    return () => ro.disconnect();\n  }, [layout]);\n\n  useEffect(() => {\n    for (const entry of entries.current) start(entry);\n  }, [start, width, duration, pause, reduce, onScreen]);\n\n  useEffect(() => {\n    const all = entries.current;\n    return () => {\n      for (const entry of all) entry.anim?.cancel();\n    };\n  }, []);\n\n  const register = useCallback<Register>(\n    (block, band) => {\n      const entry: Entry = { block, band };\n      entries.current.add(entry);\n      observer.current?.observe(block);\n      layout();\n      start(entry);\n      return () => {\n        entry.anim?.cancel();\n        entries.current.delete(entry);\n        observer.current?.unobserve(block);\n      };\n    },\n    [layout, start],\n  );\n\n  return (\n    <SweepContext.Provider value={register}>\n      <div\n        aria-busy=\"true\"\n        aria-label=\"Loading\"\n        className={cn(\n          \"[--sweep-base:color-mix(in_oklab,var(--color-foreground)_8%,transparent)] [--sweep-hi:color-mix(in_oklab,var(--color-background)_88%,transparent)] dark:[--sweep-hi:color-mix(in_oklab,var(--color-foreground)_9%,transparent)]\",\n          className,\n        )}\n        ref={ref}\n        role=\"status\"\n        style={style as CSSProperties}\n        {...props}\n      >\n        {children}\n      </div>\n    </SweepContext.Provider>\n  );\n}\n\nexport function SkeletonSweepBlock({ className, style, ...props }: ComponentProps<\"div\">) {\n  const ref = useRef<HTMLDivElement>(null);\n  const band = useRef<HTMLSpanElement>(null);\n  const register = useContext(SweepContext);\n\n  useEffect(() => {\n    if (!register || !ref.current || !band.current) return;\n    return register(ref.current, band.current);\n  }, [register]);\n\n  return (\n    <div\n      aria-hidden\n      className={cn(\"relative isolate h-3 overflow-hidden rounded-full\", className)}\n      ref={ref}\n      style={{ backgroundColor: \"var(--sweep-base, var(--color-muted))\", ...style }}\n      {...props}\n    >\n      <span\n        className=\"pointer-events-none absolute will-change-transform\"\n        ref={band}\n        style={{\n          left: \"calc(var(--sweep-bx, 0px) * -1)\",\n          top: \"calc(var(--sweep-by, 0px) * -1)\",\n          width: \"var(--sweep-w, 0px)\",\n          height: \"var(--sweep-h, 0px)\",\n          transform: \"translateX(-9999px)\",\n          backgroundImage: \"linear-gradient(105deg, transparent 38%, var(--sweep-hi, transparent) 50%, transparent 62%)\",\n        }}\n      />\n    </div>\n  );\n}\n\nexport interface SkeletonSweepTextProps extends ComponentProps<\"span\"> {\n  duration?: number;\n}\n\nexport function SkeletonSweepText({ duration = 1.8, className, children, ...props }: SkeletonSweepTextProps) {\n  const ref = useRef<HTMLSpanElement>(null);\n  const reduce = useReducedMotion();\n  const onScreen = useOnScreen(ref);\n\n  useEffect(() => {\n    const el = ref.current;\n    if (!el || !onScreen) return;\n    const ms = duration * 1000;\n    const anim = reduce\n      ? el.animate(PULSE, { duration: 1800, easing: \"ease-in-out\", iterations: Number.POSITIVE_INFINITY })\n      : el.animate(\n          [\n            { backgroundPosition: \"150% 0%\", offset: 0 },\n            { backgroundPosition: \"-50% 0%\", offset: ms / (ms + 200) },\n            { backgroundPosition: \"-50% 0%\", offset: 1 },\n          ],\n          { duration: ms + 200, easing: \"linear\", iterations: Number.POSITIVE_INFINITY },\n        );\n    return () => anim.cancel();\n  }, [duration, reduce, onScreen]);\n\n  return (\n    <span className={cn(\"relative inline-block\", className)} {...props}>\n      <span\n        className=\"inline-block bg-clip-text text-transparent\"\n        ref={ref}\n        style={{\n          backgroundImage:\n            \"linear-gradient(90deg, var(--color-muted-foreground) 0%, var(--color-muted-foreground) 38%, var(--color-foreground) 50%, var(--color-muted-foreground) 62%, var(--color-muted-foreground) 100%)\",\n          backgroundSize: \"200% 100%\",\n          backgroundPosition: \"150% 0%\",\n        }}\n      >\n        {children}\n      </span>\n    </span>\n  );\n}\n"
    }
  ]
}