{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "loading-06",
  "title": "Skeleton presets",
  "description": "Text, profile, list and media skeletons shaped like the content they stand in for, all on the shared sweep.",
  "dependencies": [],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/card.json",
    "https://ui.ndekocode.dev/r/live-region.json",
    "https://ui.ndekocode.dev/r/motion-word-roll.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-06.tsx",
      "content": "'use client';\n\nimport { createContext, useContext, useEffect, useRef, useState } from 'react';\n\nimport { WordRoll } from '@/components/core/word-roll';\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\nexport type SkeletonKind = 'text' | 'profile' | 'list' | 'media';\n\nconst KINDS: Segment<SkeletonKind>[] = [\n  { value: 'text', label: 'Text' },\n  { value: 'profile', label: 'Profile' },\n  { value: 'list', label: 'List' },\n  { value: 'media', label: 'Media' },\n];\n\nconst STANDS_IN_FOR: Record<SkeletonKind, string> = {\n  text: 'An article or a description',\n  profile: 'A person and what they do',\n  list: 'Rows of an inbox or a table',\n  media: 'A grid of covers or photos',\n};\n\nconst STANDS_IN_FOR_LABELS = KINDS.map((kind) => STANDS_IN_FOR[kind.value]);\n\nconst BODY_WIDTHS = ['w-full', 'w-11/12', 'w-full', 'w-10/12'];\n\nconst LAST_LINE_WIDTH = 'w-1/2';\n\nconst STAT_COUNT = 3;\n\nfunction lineWidthsOf(lines: number) {\n  const bodyCount = Math.max(1, lines) - 1;\n  const widths: string[] = [];\n  for (let index = 0; index < bodyCount; index += 1) {\n    const width = BODY_WIDTHS[index % BODY_WIDTHS.length];\n    widths.push(width);\n  }\n  widths.push(LAST_LINE_WIDTH);\n  return widths;\n}\n\nfunction indexesOf(count: number) {\n  const safeCount = Math.max(1, count);\n  return Array.from({ length: safeCount }, (_, index) => index);\n}\n\ntype TextSkeletonProps = {\n  lines?: number;\n};\n\nexport function TextSkeleton({ lines = 4 }: TextSkeletonProps) {\n  const widths = lineWidthsOf(lines);\n\n  return (\n    <div className='flex flex-col gap-4'>\n      <SweepBlock className='h-4 w-1/2 rounded-full' />\n      <div className='flex flex-col gap-2.5'>\n        {widths.map((width, index) => (\n          <SweepBlock\n            key={`${index}-${width}`}\n            className={cn('h-3 rounded-full', width)}\n          />\n        ))}\n      </div>\n    </div>\n  );\n}\n\nexport function ProfileSkeleton() {\n  return (\n    <div className='flex flex-col gap-4'>\n      <div className='flex flex-col'>\n        <SweepBlock className='h-20 w-full rounded-lg' />\n        <SweepBlock className='ring-card ms-4 -mt-7 size-14 rounded-full ring-4' />\n      </div>\n      <div className='flex flex-col gap-2'>\n        <SweepBlock className='h-3.5 w-1/3 rounded-full' />\n        <SweepBlock className='h-3 w-1/2 rounded-full' />\n      </div>\n      <div className='flex gap-6'>\n        {indexesOf(STAT_COUNT).map((stat) => (\n          <div key={stat} className='flex flex-col gap-1.5'>\n            <SweepBlock className='h-3.5 w-8 rounded-full' />\n            <SweepBlock className='h-2.5 w-12 rounded-full' />\n          </div>\n        ))}\n      </div>\n      <SweepBlock className='h-8 w-28 rounded-full' />\n    </div>\n  );\n}\n\ntype ListSkeletonProps = {\n  rows?: number;\n};\n\nexport function ListSkeleton({ rows = 4 }: ListSkeletonProps) {\n  return (\n    <div className='flex flex-col gap-4'>\n      {indexesOf(rows).map((row) => (\n        <div key={row} className='flex items-center 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-2/5 rounded-full' />\n            <SweepBlock className='h-2.5 w-3/4 rounded-full' />\n          </div>\n          <SweepBlock className='h-5 w-10 shrink-0 rounded-md' />\n        </div>\n      ))}\n    </div>\n  );\n}\n\ntype MediaSkeletonProps = {\n  tiles?: number;\n};\n\nexport function MediaSkeleton({ tiles = 4 }: MediaSkeletonProps) {\n  return (\n    <div className='grid grid-cols-2 gap-3'>\n      {indexesOf(tiles).map((tile) => (\n        <div key={tile} className='flex flex-col gap-2'>\n          <SweepBlock className='aspect-[3/2] w-full rounded-lg' />\n          <SweepBlock className='h-3 w-3/4 rounded-full' />\n        </div>\n      ))}\n    </div>\n  );\n}\n\ntype SkeletonPresetProps = {\n  kind: SkeletonKind;\n};\n\nexport function SkeletonPreset({ kind }: SkeletonPresetProps) {\n  if (kind === 'profile') {\n    return <ProfileSkeleton />;\n  }\n  if (kind === 'list') {\n    return <ListSkeleton />;\n  }\n  if (kind === 'media') {\n    return <MediaSkeleton />;\n  }\n  return <TextSkeleton />;\n}\n\nconst KIND_ANNOUNCEMENTS: Record<SkeletonKind, string> = {\n  text: 'Showing the text skeleton',\n  profile: 'Showing the profile skeleton',\n  list: 'Showing the list skeleton',\n  media: 'Showing the media skeleton',\n};\n\nexport default function Loading06() {\n  const [kind, setKind] = useState<SkeletonKind>('text');\n  const [announcement, setAnnouncement] = useState('');\n\n  const steps: AutoplayStep[] = [\n    { after: 2800, run: () => setKind('profile') },\n    { after: 2800, run: () => setKind('list') },\n    { after: 2800, run: () => setKind('media') },\n  ];\n  const { ref, isInView } = useAutoplay(steps, {\n    loopAfter: 2400,\n    onReset: () => setKind('text'),\n  });\n\n  useKeptHeight(ref);\n\n  function chooseKind(next: SkeletonKind) {\n    setKind(next);\n    setAnnouncement(KIND_ANNOUNCEMENTS[next]);\n  }\n\n  return (\n    <div\n      ref={ref}\n      className='flex min-h-[28rem] w-full max-w-sm flex-col gap-3'\n    >\n      <Segments\n        label='Skeleton preset'\n        value={kind}\n        segments={KINDS}\n        onValueChange={chooseKind}\n      />\n      <Card className='py-4'>\n        <CardContent className='min-h-[20rem] px-4'>\n          <SweepScope isActive={isInView}>\n            <SkeletonPreset kind={kind} />\n          </SweepScope>\n        </CardContent>\n      </Card>\n      <p className='text-muted-foreground flex min-h-4 gap-1 px-1 text-xs'>\n        <span>Stands in for</span>\n        <WordRoll\n          text={STANDS_IN_FOR[kind]}\n          labels={STANDS_IN_FOR_LABELS}\n          className='text-foreground'\n        />\n      </p>\n      <LiveRegion message={announcement} />\n    </div>\n  );\n}\n",
      "type": "registry:component"
    }
  ],
  "categories": [
    "components",
    "loading"
  ],
  "type": "registry:component"
}