{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "loading-05",
  "title": "One sweep for every skeleton",
  "description": "Skeleton blocks that share one light passing over the whole layout in step with each other, so a page that changes shape keeps the same light.",
  "dependencies": [],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/card.json",
    "https://ui.ndekocode.dev/r/live-region.json",
    "https://ui.ndekocode.dev/r/segments.json",
    "https://ui.ndekocode.dev/r/theme.json",
    "https://ui.ndekocode.dev/r/use-autoplay.json",
    "https://ui.ndekocode.dev/r/use-kept-height.json",
    "https://ui.ndekocode.dev/r/use-prefers-reduced-motion.json",
    "https://ui.ndekocode.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/ndeko/components/loading/loading-05.tsx",
      "content": "'use client';\n\nimport { createContext, useContext, useEffect, useRef, useState } from 'react';\n\nimport {\n  useAutoplay,\n  type AutoplayStep,\n} from '@/registry/ndeko/hooks/use-autoplay';\nimport { useKeptHeight } from '@/registry/ndeko/hooks/use-kept-height';\nimport { usePrefersReducedMotion } from '@/registry/ndeko/hooks/use-prefers-reduced-motion';\nimport { cn } from '@/registry/ndeko/lib/utils';\nimport { Card, CardContent } from '@/registry/ndeko/ui/card';\nimport { LiveRegion } from '@/registry/ndeko/ui/live-region';\nimport { Segments, type Segment } from '@/registry/ndeko/ui/segments';\n\ntype SweepLight = 'sweeping' | 'still';\n\nconst SweepContext = createContext<SweepLight>('still');\n\nconst BAND_WIDTH_PX = 280;\nconst SPEED_PX_PER_MS = 0.7;\nconst PASS_SHARE = 0.72;\n\nconst BLOCK_BASE = [\n  'relative block overflow-hidden rounded-md',\n  '[--sweep-light:color-mix(in_oklab,var(--card)_85%,transparent)] dark:[--sweep-light:color-mix(in_oklab,var(--foreground)_10%,transparent)]',\n].join(' ');\n\nconst BAND =\n  'pointer-events-none absolute inset-y-0 left-0 w-[280px] bg-[linear-gradient(90deg,transparent,var(--sweep-light),transparent)]';\n\nfunction tintOf(light: SweepLight) {\n  if (light === 'sweeping') {\n    return 'bg-foreground/[0.06]';\n  }\n  return 'bg-foreground/[0.08]';\n}\n\nfunction lightOf(isSweeping: boolean): SweepLight {\n  if (isSweeping) {\n    return 'sweeping';\n  }\n  return 'still';\n}\n\nfunction keyframesOf(blockLeft: number, viewportWidth: number): Keyframe[] {\n  const start = -BAND_WIDTH_PX - blockLeft;\n  const end = viewportWidth - blockLeft;\n  const from = `${start}px 0`;\n  const to = `${end}px 0`;\n  return [\n    { translate: from, offset: 0 },\n    { translate: to, offset: PASS_SHARE },\n    { translate: to, offset: 1 },\n  ];\n}\n\nfunction periodOf(viewportWidth: number) {\n  const travel = viewportWidth + BAND_WIDTH_PX;\n  const passMs = travel / SPEED_PX_PER_MS;\n  return Math.round(passMs / PASS_SHARE);\n}\n\n/**\n * Every band runs on the document timeline (start time 0), so all of them are at the same phase,\n * and each is shifted by the left edge of its own block, so they draw one light across the page.\n */\nfunction SweepBand() {\n  const bandRef = useRef<HTMLSpanElement>(null);\n\n  useEffect(() => {\n    const band = bandRef.current;\n    const block = band?.parentElement;\n    if (!band || !block) {\n      return;\n    }\n    let animation: Animation | undefined;\n\n    const restart = () => {\n      animation?.cancel();\n      const { left } = block.getBoundingClientRect();\n      const width = window.innerWidth;\n      const keyframes = keyframesOf(left, width);\n      animation = band.animate(keyframes, {\n        duration: periodOf(width),\n        iterations: Infinity,\n      });\n      animation.startTime = 0;\n    };\n\n    restart();\n    const observer = new ResizeObserver(restart);\n    observer.observe(block);\n    window.addEventListener('resize', restart);\n\n    return () => {\n      animation?.cancel();\n      observer.disconnect();\n      window.removeEventListener('resize', restart);\n    };\n  }, []);\n\n  return <span ref={bandRef} aria-hidden='true' className={BAND} />;\n}\n\ntype SweepScopeProps = {\n  isActive: boolean;\n  children: React.ReactNode;\n};\n\nexport function SweepScope({ isActive, children }: SweepScopeProps) {\n  const shouldReduceMotion = usePrefersReducedMotion();\n  const isSweeping = isActive && !shouldReduceMotion;\n  const light = lightOf(isSweeping);\n\n  return (\n    <SweepContext.Provider value={light}>{children}</SweepContext.Provider>\n  );\n}\n\nexport function SweepBlock({ className }: { className?: string }) {\n  const light = useContext(SweepContext);\n  const isSweeping = light === 'sweeping';\n\n  return (\n    <span\n      data-slot='sweep-block'\n      aria-hidden='true'\n      className={cn(BLOCK_BASE, tintOf(light), className)}\n    >\n      {isSweeping && <SweepBand />}\n    </span>\n  );\n}\n\ntype PageKind = 'profile' | 'feed' | 'gallery';\n\nconst PAGE_KINDS: Segment<PageKind>[] = [\n  { value: 'profile', label: 'Profile' },\n  { value: 'feed', label: 'Feed' },\n  { value: 'gallery', label: 'Gallery' },\n];\n\nconst FEED_ROWS = [0, 1, 2];\n\nconst GALLERY_TILES = [0, 1, 2, 3];\n\nfunction ProfileSkeleton() {\n  return (\n    <div className='flex flex-col gap-4'>\n      <div className='flex items-center gap-3'>\n        <SweepBlock className='size-10 shrink-0 rounded-full' />\n        <div className='flex min-w-0 flex-1 flex-col gap-2'>\n          <SweepBlock className='h-3 w-2/5 rounded-full' />\n          <SweepBlock className='h-2.5 w-1/4 rounded-full' />\n        </div>\n      </div>\n      <div className='flex flex-col gap-2'>\n        <SweepBlock className='h-3 w-full rounded-full' />\n        <SweepBlock className='h-3 w-11/12 rounded-full' />\n        <SweepBlock className='h-3 w-3/5 rounded-full' />\n      </div>\n      <SweepBlock className='h-24 w-full rounded-lg' />\n      <div className='flex gap-2'>\n        <SweepBlock className='h-8 w-24 rounded-full' />\n        <SweepBlock className='h-8 w-20 rounded-full' />\n      </div>\n    </div>\n  );\n}\n\nfunction FeedSkeleton() {\n  return (\n    <div className='flex flex-col gap-5'>\n      {FEED_ROWS.map((row) => (\n        <div key={row} className='flex items-start gap-3'>\n          <SweepBlock className='size-8 shrink-0 rounded-full' />\n          <div className='flex min-w-0 flex-1 flex-col gap-2'>\n            <SweepBlock className='h-3 w-1/3 rounded-full' />\n            <SweepBlock className='h-3 w-full rounded-full' />\n            <SweepBlock className='h-3 w-4/5 rounded-full' />\n          </div>\n        </div>\n      ))}\n    </div>\n  );\n}\n\nfunction GallerySkeleton() {\n  return (\n    <div className='grid grid-cols-2 gap-3'>\n      {GALLERY_TILES.map((tile) => (\n        <div key={tile} className='flex flex-col gap-2'>\n          <SweepBlock className='h-20 w-full rounded-lg' />\n          <SweepBlock className='h-3 w-3/4 rounded-full' />\n        </div>\n      ))}\n    </div>\n  );\n}\n\nfunction PageSkeleton({ kind }: { kind: PageKind }) {\n  if (kind === 'feed') {\n    return <FeedSkeleton />;\n  }\n  if (kind === 'gallery') {\n    return <GallerySkeleton />;\n  }\n  return <ProfileSkeleton />;\n}\n\nconst KIND_ANNOUNCEMENTS: Record<PageKind, string> = {\n  profile: 'Showing the profile skeleton',\n  feed: 'Showing the feed skeleton',\n  gallery: 'Showing the gallery skeleton',\n};\n\nexport default function Loading05() {\n  const [kind, setKind] = useState<PageKind>('profile');\n  const [announcement, setAnnouncement] = useState('');\n\n  const steps: AutoplayStep[] = [\n    { after: 3200, run: () => setKind('feed') },\n    { after: 3200, run: () => setKind('gallery') },\n    { after: 3200, run: () => setKind('profile') },\n  ];\n  const { ref, isInView } = useAutoplay(steps, {\n    loopAfter: 2400,\n    onReset: () => setKind('profile'),\n  });\n\n  useKeptHeight(ref);\n\n  function chooseKind(next: PageKind) {\n    setKind(next);\n    setAnnouncement(KIND_ANNOUNCEMENTS[next]);\n  }\n\n  return (\n    <div\n      ref={ref}\n      className='flex min-h-[26rem] w-full max-w-sm flex-col gap-3'\n    >\n      <Segments\n        label='Page skeleton'\n        value={kind}\n        segments={PAGE_KINDS}\n        onValueChange={chooseKind}\n      />\n      <Card className='py-4'>\n        <CardContent className='min-h-[20rem] px-4'>\n          <SweepScope isActive={isInView}>\n            <PageSkeleton kind={kind} />\n          </SweepScope>\n        </CardContent>\n      </Card>\n      <LiveRegion message={announcement} />\n    </div>\n  );\n}\n",
      "type": "registry:component"
    }
  ],
  "categories": [
    "components",
    "loading"
  ],
  "type": "registry:component"
}