{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "skeleton-orchestra",
  "title": "Skeleton Orchestra",
  "author": "WebberUI",
  "description": "骨架屏交響系統：載入完成時，骨架依版面位置以方向性波次逐一實體化為真實內容。",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/webber/skeleton-orchestra.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface OrchestraContextValue {\n  loading: boolean;\n  stagger: number;\n  isStatic: boolean;\n  register: (id: string) => () => void;\n  order: string[];\n}\n\nconst OrchestraContext = React.createContext<OrchestraContextValue | null>(\n  null\n);\n\nconst EASE_OUT: [number, number, number, number] = [0.22, 1, 0.36, 1];\n\ninterface SkeletonOrchestraProps {\n  /** 是否處於載入狀態；由 true 轉為 false 時觸發波次實體化 */\n  loading: boolean;\n  /** 相鄰波次的間隔（秒） */\n  stagger?: number;\n  /** 放入含 `SkeletonSlot` 的版面內容 */\n  children: React.ReactNode;\n  className?: string;\n}\n\nexport function SkeletonOrchestra({\n  loading,\n  stagger = 0.1,\n  children,\n  className,\n}: SkeletonOrchestraProps) {\n  const reducedMotion = useReducedMotion();\n  // 首次渲染必須與 SSR 輸出一致，掛載後才切換到 reduced-motion 靜態版\n  const [mounted, setMounted] = React.useState(false);\n  React.useEffect(() => setMounted(true), []);\n  const isStatic = Boolean(mounted && reducedMotion);\n\n  // 依掛載順序（即 DOM 順序，左上到右下）記錄各 Slot，作為自動波次序\n  const [order, setOrder] = React.useState<string[]>([]);\n  const register = React.useCallback((id: string) => {\n    setOrder((prev) => (prev.includes(id) ? prev : [...prev, id]));\n    return () =>\n      setOrder((prev) => prev.filter((slotId) => slotId !== id));\n  }, []);\n\n  const contextValue = React.useMemo<OrchestraContextValue>(\n    () => ({ loading, stagger, isStatic, register, order }),\n    [loading, stagger, isStatic, register, order]\n  );\n\n  return (\n    <OrchestraContext.Provider value={contextValue}>\n      <div className={className} aria-busy={loading}>\n        {children}\n      </div>\n    </OrchestraContext.Provider>\n  );\n}\n\ninterface SkeletonSlotProps {\n  /** 手動指定波次序（0 起算）；未指定時依 Slot 的掛載順序自動編號 */\n  index?: number;\n  /** 自訂骨架形狀；未指定時依 children 尺寸畫圓角灰塊 */\n  skeleton?: React.ReactNode;\n  /** 真實內容 */\n  children: React.ReactNode;\n  className?: string;\n}\n\nexport function SkeletonSlot({\n  index,\n  skeleton,\n  children,\n  className,\n}: SkeletonSlotProps) {\n  const ctx = React.useContext(OrchestraContext);\n  const id = React.useId();\n  const register = ctx?.register;\n\n  React.useEffect(() => {\n    if (!register) return;\n    return register(id);\n  }, [register, id]);\n\n  // 未包在 SkeletonOrchestra 內時，直接渲染真實內容\n  if (!ctx) {\n    return <div className={className}>{children}</div>;\n  }\n\n  const { loading, stagger, isStatic, order } = ctx;\n  const autoIndex = order.indexOf(id);\n  const waveIndex = index ?? Math.max(0, autoIndex);\n  const delay = waveIndex * stagger;\n\n  const skeletonNode = skeleton ?? (\n    <div className=\"rounded-md bg-neutral-200 dark:bg-neutral-800\">\n      {/* 隱形佔位：讓灰塊取得與真實內容相同的尺寸，骨架與最終版面同構 */}\n      <div className=\"invisible\" aria-hidden>\n        {children}\n      </div>\n    </div>\n  );\n\n  // 使用者偏好減少動態效果時：無波次、無縮放，骨架與內容直接切換\n  if (isStatic) {\n    return (\n      <div className={className}>\n        {loading ? (\n          <div aria-hidden className=\"motion-reduce:animate-none\">\n            {skeletonNode}\n          </div>\n        ) : (\n          children\n        )}\n      </div>\n    );\n  }\n\n  return (\n    <div className={cn(\"relative\", className)}>\n      <AnimatePresence mode=\"popLayout\" initial={false}>\n        {loading ? (\n          <motion.div\n            key=\"skeleton\"\n            aria-hidden\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1, transition: { duration: 0.2 } }}\n            exit={{\n              opacity: 0,\n              scale: 0.95,\n              transition: { delay, duration: 0.3, ease: EASE_OUT },\n            }}\n          >\n            {/* 脈動放在內層，避免與外層 exit 的 opacity 動畫互相覆蓋 */}\n            <div className=\"animate-pulse motion-reduce:animate-none\">\n              {skeletonNode}\n            </div>\n          </motion.div>\n        ) : (\n          <motion.div\n            key=\"content\"\n            initial={{ opacity: 0, scale: 0.92 }}\n            animate={{\n              opacity: 1,\n              scale: 1,\n              transition: { delay, duration: 0.45, ease: EASE_OUT },\n            }}\n            exit={{ opacity: 0, transition: { duration: 0.15 } }}\n          >\n            {children}\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "loading",
    "system"
  ],
  "type": "registry:ui"
}