{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "live-glance-strip",
  "title": "Live Glance Strip",
  "author": "WebberUI",
  "description": "即時動態資訊條：iOS Live Activity 語彙的毛玻璃膠囊，狀態文字上下翻轉更新、進度軌道即時推進，點擊展開詳細內容。",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/webber/live-glance-strip.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nconst EASE_OUT: [number, number, number, number] = [0.22, 1, 0.36, 1];\n\n/** 膠囊長高／收合的 layout spring */\nconst LAYOUT_SPRING = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 34,\n} as const;\n\n/** 狀態文字上下翻轉的過渡：y 走 spring、透明度走短 tween */\nconst FLIP_TRANSITION = {\n  y: { type: \"spring\", stiffness: 520, damping: 36 },\n  opacity: { duration: 0.16 },\n} as const;\n\n/** 進度軌道 scaleX 推進的 spring */\nconst PROGRESS_SPRING = {\n  type: \"spring\",\n  stiffness: 140,\n  damping: 26,\n} as const;\n\ninterface LiveGlanceStripProps {\n  /** 左側插槽內容（通常是圖示），會置於圓形底座中 */\n  icon?: React.ReactNode;\n  /** 活動標題 */\n  title: string;\n  /** 即時狀態文字；值變更時舊值向上翻出、新值自下方翻入 */\n  status: string;\n  /** 進度（0–1）；提供時顯示底部細進度軌道，變更時以 spring 推進 */\n  progress?: number;\n  /** 受控展開狀態；未提供時由元件內部管理 */\n  expanded?: boolean;\n  /** 展開狀態變更時的回呼 */\n  onExpandedChange?: (expanded: boolean) => void;\n  /** 定位模式：`fixed-bottom` 固定於視窗底部置中（z-50），`inline` 隨文件流排版 */\n  position?: \"fixed-bottom\" | \"inline\";\n  /** 展開後顯示的詳細內容；未提供時膠囊不可展開 */\n  children?: React.ReactNode;\n  /** 附加到膠囊本體的 class */\n  className?: string;\n}\n\nexport function LiveGlanceStrip({\n  icon,\n  title,\n  status,\n  progress,\n  expanded,\n  onExpandedChange,\n  position = \"inline\",\n  children,\n  className,\n}: LiveGlanceStripProps) {\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  // 受控 / 非受控展開狀態\n  const [internalExpanded, setInternalExpanded] = React.useState(false);\n  const isControlled = expanded !== undefined;\n  const isExpanded = isControlled ? expanded : internalExpanded;\n  const setExpanded = React.useCallback(\n    (next: boolean) => {\n      if (!isControlled) setInternalExpanded(next);\n      onExpandedChange?.(next);\n    },\n    [isControlled, onExpandedChange]\n  );\n\n  const expandable = children != null;\n  const detailId = React.useId();\n  const clampedProgress =\n    progress === undefined ? undefined : Math.min(1, Math.max(0, progress));\n\n  const rowContent = (\n    <>\n      {icon != null && (\n        <span\n          aria-hidden\n          className=\"flex size-8 shrink-0 items-center justify-center rounded-full bg-neutral-900/[0.06] text-neutral-700 dark:bg-white/10 dark:text-neutral-200\"\n        >\n          {icon}\n        </span>\n      )}\n      <span className=\"min-w-0 flex-1 truncate text-sm font-medium text-neutral-900 dark:text-neutral-100\">\n        {title}\n      </span>\n      {/* 狀態翻轉窗：overflow-hidden 作遮罩，舊值上翻出、新值下翻入 */}\n      <span className=\"relative shrink-0 overflow-hidden\" aria-hidden>\n        <AnimatePresence mode=\"popLayout\" initial={false}>\n          <motion.span\n            key={status}\n            initial={isStatic ? { opacity: 0 } : { y: \"110%\", opacity: 0 }}\n            animate={isStatic ? { opacity: 1 } : { y: \"0%\", opacity: 1 }}\n            exit={isStatic ? { opacity: 0 } : { y: \"-110%\", opacity: 0 }}\n            transition={isStatic ? { duration: 0.2 } : FLIP_TRANSITION}\n            className=\"block text-xs font-semibold tabular-nums text-neutral-500 dark:text-neutral-400\"\n          >\n            {status}\n          </motion.span>\n        </AnimatePresence>\n      </span>\n    </>\n  );\n\n  const rowClass = cn(\n    \"flex w-full items-center gap-3 px-4 py-3 text-left\",\n    expandable &&\n      \"cursor-pointer transition-colors duration-[var(--wb-duration-fast,200ms)] hover:bg-neutral-900/[0.03] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-neutral-400 dark:hover:bg-white/5 dark:focus-visible:ring-neutral-500\"\n  );\n\n  const capsule = (\n    <motion.div\n      layout={!isStatic}\n      transition={LAYOUT_SPRING}\n      style={{ borderRadius: 24 }}\n      className={cn(\n        // 毛玻璃膠囊：半透明底 + backdrop-blur，深淺色皆保留穿透感\n        \"overflow-hidden border border-neutral-900/10 bg-white/70 shadow-lg shadow-neutral-900/5 backdrop-blur-xl dark:border-white/10 dark:bg-neutral-900/70 dark:shadow-black/30\",\n        position === \"fixed-bottom\"\n          ? \"pointer-events-auto w-[22rem] max-w-full\"\n          : \"relative w-full max-w-sm\",\n        className\n      )}\n    >\n      {/* 輔助科技唸的是這份純文字狀態，動畫節點一律 aria-hidden */}\n      <span className=\"sr-only\" role=\"status\">\n        {title}：{status}\n      </span>\n\n      {/* 內層 layout=\"position\"：膠囊長高時內容只位移、不被拉伸 */}\n      <motion.div layout={isStatic ? false : \"position\"}>\n        {expandable ? (\n          <button\n            type=\"button\"\n            onClick={() => setExpanded(!isExpanded)}\n            aria-expanded={isExpanded}\n            aria-controls={detailId}\n            className={rowClass}\n          >\n            {rowContent}\n          </button>\n        ) : (\n          <div className={rowClass}>{rowContent}</div>\n        )}\n\n        {clampedProgress !== undefined && (\n          <div\n            role=\"progressbar\"\n            aria-valuemin={0}\n            aria-valuemax={100}\n            aria-valuenow={Math.round(clampedProgress * 100)}\n            className=\"h-0.5 w-full bg-neutral-900/10 dark:bg-white/10\"\n          >\n            <motion.div\n              className=\"h-full w-full origin-left bg-neutral-900 dark:bg-neutral-100\"\n              initial={{ scaleX: 0 }}\n              animate={{ scaleX: clampedProgress }}\n              transition={isStatic ? { duration: 0 } : PROGRESS_SPRING}\n            />\n          </div>\n        )}\n      </motion.div>\n\n      <AnimatePresence initial={false}>\n        {expandable && isExpanded && (\n          <motion.div\n            key=\"detail\"\n            id={detailId}\n            layout={isStatic ? false : \"position\"}\n            initial={{ opacity: 0 }}\n            animate={{\n              opacity: 1,\n              transition: { duration: 0.28, delay: 0.08, ease: EASE_OUT },\n            }}\n            exit={{ opacity: 0, transition: { duration: 0.12 } }}\n          >\n            <div className=\"border-t border-neutral-900/5 px-4 py-3 dark:border-white/5\">\n              {children}\n            </div>\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </motion.div>\n  );\n\n  if (position === \"fixed-bottom\") {\n    return (\n      // 外層不吃指標事件，避免整條水平帶擋住頁面點擊\n      <div className=\"pointer-events-none fixed inset-x-0 bottom-4 z-50 flex justify-center px-4\">\n        {capsule}\n      </div>\n    );\n  }\n\n  return capsule;\n}\n",
      "type": "registry:ui"
    }
  ],
  "cssVars": {
    "light": {
      "wb-duration-fast": "200ms"
    }
  },
  "categories": [
    "feedback",
    "data-display"
  ],
  "type": "registry:ui"
}