{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "orbit",
  "title": "轨道环绕",
  "description": "可组合的同心轨道与正向节点，支持半弧、反向运行与离屏暂停。",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://yiui.dev/r/flow-diagram.json",
    "https://yiui.dev/r/flow-tone.json"
  ],
  "files": [
    {
      "path": "src/registry/ui/orbit/orbit.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  motion,\n  useInView,\n  useMotionValue,\n  useTransform,\n  type MotionValue,\n} from \"motion/react\"\nimport { cn } from \"@/lib/utils\"\nimport {\n  backgroundClasses,\n  FADE_MASK,\n  type FlowBackgroundName,\n} from \"@/components/ui/flow-diagram/background\"\nimport {\n  flowToneStyle,\n  type FlowTone,\n  type FlowToneName,\n} from \"@/lib/flow-tone\"\n\n// 订阅系统偏好的实时变更，服务端始终使用静帧。\nfunction subscribeReducedMotion(callback: () => void) {\n  const media = window.matchMedia(\"(prefers-reduced-motion: reduce)\")\n  media.addEventListener(\"change\", callback)\n  return () => media.removeEventListener(\"change\", callback)\n}\nfunction reducedMotionSnapshot() {\n  return window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n}\nfunction serverReducedMotionSnapshot() {\n  return true\n}\nfunction useOrbitReducedMotion() {\n  return React.useSyncExternalStore(\n    subscribeReducedMotion,\n    reducedMotionSnapshot,\n    serverReducedMotionSnapshot,\n  )\n}\n\ntype OrbitArc = \"full\" | \"half\"\n/** 画布在外框里的位置：`bottom` 让轨道中心落在外框底边，配合 `arc=\"half\"` 得到贴地的半圆。 */\ntype OrbitAnchor = \"center\" | \"bottom\"\n\n/**\n * 调整用的覆盖项：`Orbit` 子树里所有轨道的对应属性被这里的值替换。\n * 用于调试面板一类的场景；不包 `OrbitTuningContext.Provider` 时组件行为不变。\n */\ntype OrbitTuning = {\n  /** 速度倍率，轨道实际用时为 `duration / speed`。 */\n  speed?: number\n  /** 为 true 时所有轨道的方向取反。 */\n  flip?: boolean\n  /** 覆盖根的 `paused`。 */\n  paused?: boolean\n  /** 覆盖每条轨道的 `dashed`。 */\n  dashed?: boolean\n  /** 覆盖每条轨道的 `tone`；`false` 表示不上色，沿用主题线条色。 */\n  tone?: FlowTone | false\n  /** 覆盖根的 `background`。 */\n  background?: FlowBackgroundName\n  /** 回报示例自己的实际取值（未被覆盖前），面板据此显示当前值。 */\n  onReport?: (values: OrbitReport) => void\n}\n\ntype OrbitReport = {\n  paused: boolean\n  background: FlowBackgroundName\n}\n\nconst OrbitTuningContext = React.createContext<OrbitTuning>({})\n\nconst OrbitContext = React.createContext<{\n  active: boolean\n  size: number\n  speed: number\n  flip: boolean\n  dashed?: boolean\n  tone?: FlowTone | false\n}>({ active: false, size: 320, speed: 1, flip: false })\nconst OrbitRingContext = React.createContext<{\n  angle: MotionValue<number>\n} | null>(null)\n\nfunction Orbit({\n  size = 320,\n  paused: pausedProp = false,\n  background: backgroundProp = \"dots\",\n  anchor = \"center\",\n  label,\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  /** 设计画布边长；半径以此为坐标，窄容器内自动缩小。 */\n  size?: number\n  paused?: boolean\n  background?: FlowBackgroundName\n  /** 画布位置；`bottom` 把轨道中心放到外框底边，上半部分留在可视区内。@default \"center\" */\n  anchor?: OrbitAnchor\n  /** 传入说明后作为图片朗读；默认作为装饰隐藏。内部不应放交互元素。 */\n  label?: string\n}) {\n  const ref = React.useRef<HTMLDivElement>(null)\n  const inView = useInView(ref, { amount: 0.2 })\n  const reduced = useOrbitReducedMotion()\n  const tuning = React.useContext(OrbitTuningContext)\n  const { onReport } = tuning\n  const paused = tuning.paused ?? pausedProp\n  const background = tuning.background ?? backgroundProp\n  React.useEffect(() => {\n    onReport?.({ paused: pausedProp, background: backgroundProp })\n  }, [onReport, pausedProp, backgroundProp])\n  return (\n    <div\n      {...props}\n      ref={ref}\n      data-slot=\"orbit\"\n      data-anchor={anchor}\n      role={label ? \"img\" : undefined}\n      aria-label={label}\n      aria-hidden={label ? undefined : true}\n      className={cn(\n        \"relative flex h-72 w-full items-center justify-center overflow-hidden\",\n        anchor === \"bottom\" && \"items-end\",\n        className,\n      )}\n    >\n      <div\n        data-slot=\"orbit-background\"\n        className={cn(\n          \"pointer-events-none absolute inset-0\",\n          backgroundClasses[background],\n          FADE_MASK,\n        )}\n      />\n      <div\n        data-slot=\"orbit-canvas\"\n        className={cn(\n          \"relative aspect-square shrink-0\",\n          anchor === \"bottom\" && \"translate-y-1/2\",\n        )}\n        style={{ width: `min(100%, ${Math.max(1, size)}px)` }}\n      >\n        <OrbitContext.Provider\n          value={{\n            active: inView && !paused && !reduced,\n            size: Math.max(1, size),\n            speed: Math.max(0.01, tuning.speed ?? 1),\n            flip: tuning.flip ?? false,\n            dashed: tuning.dashed,\n            tone: tuning.tone,\n          }}\n        >\n          {children}\n        </OrbitContext.Provider>\n      </div>\n    </div>\n  )\n}\n\nfunction OrbitRing({\n  radius = 100,\n  duration = 24,\n  reverse: reverseProp = false,\n  paused = false,\n  startAngle = -90,\n  arc = \"full\",\n  dashed: dashedProp = false,\n  tone: toneProp,\n  className,\n  style,\n  children,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  radius?: number\n  /** 完整转一圈的秒数。 */\n  duration?: number\n  reverse?: boolean\n  paused?: boolean\n  /** 起始角度，0 在右侧，正值顺时针。 */\n  startAngle?: number\n  /** half 只显示上半圆，节点在下半圆时隐藏，边缘渐隐。 */\n  arc?: OrbitArc\n  dashed?: boolean\n  /** 轨道线的色调：内置名或任意 CSS 颜色；不传沿用主题的线条色。节点的颜色由各自的 `tone` 决定。 */\n  tone?: FlowTone\n}) {\n  const { active, size, speed, flip, ...overrides } =\n    React.useContext(OrbitContext)\n  const reverse = flip ? !reverseProp : reverseProp\n  const dashed = overrides.dashed ?? dashedProp\n  const tone =\n    overrides.tone === false ? undefined : (overrides.tone ?? toneProp)\n  const angle = useMotionValue(startAngle)\n  const reduced = useOrbitReducedMotion()\n  React.useEffect(() => {\n    angle.set(startAngle)\n  }, [angle, startAngle, reduced])\n  React.useEffect(() => {\n    if (!active || paused) return\n    let last = performance.now()\n    let frame = requestAnimationFrame(function tick(now) {\n      angle.set(\n        angle.get() +\n          (((Math.min(now - last, 100) / 1000) * 360) /\n            Math.max(0.1, duration / speed)) *\n            (reverse ? -1 : 1),\n      )\n      last = now\n      frame = requestAnimationFrame(tick)\n    })\n    return () => cancelAnimationFrame(frame)\n  }, [active, paused, duration, speed, reverse, angle])\n  const diameter = ((Math.max(0, radius) * 2) / size) * 100\n  return (\n    <div\n      {...props}\n      data-slot=\"orbit-ring\"\n      data-arc={arc}\n      className={cn(\n        \"pointer-events-none absolute inset-0\",\n        arc === \"half\" &&\n          \"[mask-image:linear-gradient(to_bottom,black_40%,transparent_52%)]\",\n        className,\n      )}\n      style={style}\n    >\n      <div\n        data-slot=\"orbit-path\"\n        className={cn(\n          \"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full border\",\n          tone ? \"border-(--flow-tone)/40\" : \"border-edge\",\n          dashed && \"border-dashed\",\n        )}\n        style={{\n          width: `${diameter}%`,\n          height: `${diameter}%`,\n          ...flowToneStyle(tone),\n        }}\n      >\n        <OrbitRingContext.Provider value={{ angle }}>\n          {children}\n        </OrbitRingContext.Provider>\n      </div>\n    </div>\n  )\n}\n\nfunction OrbitItem({\n  angle: offset = 0,\n  tone = \"foreground\",\n  className,\n  style,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  /** 同一轨道内节点之间的角度偏移。 */\n  angle?: number\n  tone?: FlowToneName\n}) {\n  const ring = React.useContext(OrbitRingContext)\n  if (!ring) throw new Error(\"OrbitItem 必须放在 OrbitRing 内\")\n  const x = useTransform(\n    ring.angle,\n    (value) =>\n      `${(Math.cos(((value + offset) * Math.PI) / 180) * 50).toFixed(3)}%`,\n  )\n  const y = useTransform(\n    ring.angle,\n    (value) =>\n      `${(Math.sin(((value + offset) * Math.PI) / 180) * 50).toFixed(3)}%`,\n  )\n  return (\n    <motion.div\n      data-slot=\"orbit-item-position\"\n      className=\"absolute inset-0 flex items-center justify-center\"\n      style={{ x, y }}\n    >\n      <div\n        {...props}\n        data-slot=\"orbit-item\"\n        className={cn(\n          \"flex size-10 shrink-0 items-center justify-center rounded-lg border border-edge bg-background font-mono text-xs whitespace-nowrap text-(--flow-tone) [&_svg]:size-4\",\n          className,\n        )}\n        style={{ ...flowToneStyle(tone), ...style }}\n      >\n        {props.children}\n      </div>\n    </motion.div>\n  )\n}\n\nfunction OrbitCenter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      {...props}\n      data-slot=\"orbit-center\"\n      className={cn(\n        \"absolute top-1/2 left-1/2 z-10 flex -translate-x-1/2 -translate-y-1/2 items-center justify-center\",\n        className,\n      )}\n    />\n  )\n}\n\nexport { Orbit, OrbitRing, OrbitItem, OrbitCenter, OrbitTuningContext }\nexport type { OrbitAnchor, OrbitArc, OrbitReport, OrbitTuning }\n",
      "type": "registry:ui",
      "target": "@ui/orbit/orbit.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-edge": "var(--edge)"
    },
    "light": {
      "edge": "color-mix(in oklch, var(--foreground) 12%, transparent)"
    },
    "dark": {
      "edge": "color-mix(in oklch, var(--foreground) 18%, transparent)"
    }
  },
  "type": "registry:ui"
}
