TL;DR:不要直接给 TabsContent 做动画(隐藏时会 display: none,退出动画放不出来)。把 Tabs 做成受控组件,用 AnimatePresence 只渲染一个当前面板;根据“上一个激活值”和“当前激活值”的顺序差决定左右方向。本文提供一套通用组件 <AnimatedPanels /> 与完整示例,可直接复用到 shadcn/ui、Radix、Headless UI 或自研 Tabs。


背景 & 问题

很多 UI 库(包括 shadcn/ui 基于的 Radix Tabs)在非激活时会把面板 display: none。这会导致两个问题:

  1. 退出动画失效:面板一旦 display: noneexit 动画根本没机会播放。
  2. 多实例切换撕裂:如果你把所有 TabsContent 都渲染出来,再用条件类控制显示/隐藏,会带来复杂度和不必要的 DOM 重量。

解决策略:用 “单实例面板切换” 模式(Single-instance switching)

  • Tabs 改为受控value/onValueChange),记录“上一个 tab”。
  • 只渲染一个面板(当前激活的),放进 AnimatePresence
  • 面板使用 position: absolute 做进场,position: relative 时居中。
  • 根据“方向”(左/右)设置 enter / exitx 位移与 opacity
  • 容器设 position: relative; min-height,避免高度跳动。

通用实现

组件 API 设计

type Key = string | number;

interface AnimatedPanelsProps {
  /** 当前激活面板的 key */
  active: Key;
  /** 用于判定方向的顺序列表(从左到右) */
  order: Key[];
  /** 面板内容,Record 形式可直接按 key 渲染 */
  panels: Record<Key, React.ReactNode>;

  /** 进出场位移,默认 48(px) */
  distance?: number;
  /** 自定义过渡 */
  transition?: {
    type?: "spring" | "tween";
    stiffness?: number;
    damping?: number;
    mass?: number;
    duration?: number;
  };
  /** 当系统偏好“减少动效”时仅做淡入淡出(默认自动检测) */
  respectReducedMotion?: boolean;

  /** 可选:容器类名/面板类名 */
  className?: string;
  panelClassName?: string;

  /** 可选:为可访问性设置 aria-labelledby 的 id 前缀,如 tab-installation */
  labelledByPrefix?: string;
}

可复用组件源码(拷贝即用)

依赖:React、Framer Motion v11(motion/react 导入路径)
若你仍在使用 v10,请把 import { motion, AnimatePresence } from "motion/react" 改为 from "framer-motion"

"use client";

import React from "react";
import { motion, AnimatePresence } from "motion/react";

/** 安全的“上一次值” */
function usePrevious<T>(value: T) {
  const ref = React.useRef<T>(value);
  React.useEffect(() => {
    ref.current = value;
  }, [value]);
  return ref.current;
}

/** SSR 安全的 prefers-reduced-motion 检测 */
function usePrefersReducedMotion() {
  const [reduced, setReduced] = React.useState(false);
  React.useEffect(() => {
    if (typeof window === "undefined") return;
    const m = window.matchMedia("(prefers-reduced-motion: reduce)");
    const handler = (e: MediaQueryListEvent) => setReduced(e.matches);
    setReduced(m.matches);
    if (m.addEventListener) m.addEventListener("change", handler);
    else m.addListener(handler);
    return () => {
      if (m.removeEventListener) m.removeEventListener("change", handler);
      else m.removeListener(handler);
    };
  }, []);
  return reduced;
}

type Key = string | number;

interface AnimatedPanelsProps {
  active: Key;
  order: Key[];
  panels: Record<Key, React.ReactNode>;
  distance?: number;
  transition?: {
    type?: "spring" | "tween";
    stiffness?: number;
    damping?: number;
    mass?: number;
    duration?: number;
  };
  respectReducedMotion?: boolean;
  className?: string;
  panelClassName?: string;
  labelledByPrefix?: string;
}

export function AnimatedPanels({
  active,
  order,
  panels,
  distance = 48,
  transition = { type: "spring", stiffness: 220, damping: 28, mass: 0.8 },
  respectReducedMotion = true,
  className,
  panelClassName,
  labelledByPrefix,
}: AnimatedPanelsProps) {
  const prev = usePrevious(active);
  const prefersReduced = usePrefersReducedMotion();
  const reduce = respectReducedMotion && prefersReduced;

  // 计算方向:向后(右侧)为 +1,向前(左侧)为 -1,相同时为 0
  const currIdx = order.indexOf(active);
  const prevIdx = order.indexOf(prev);
  const hasPrev = prev !== undefined && prev !== null && prevIdx !== -1;
  const diff = hasPrev ? currIdx - prevIdx : 0;
  const direction = diff === 0 ? 0 : Math.sign(diff); // -1 | 0 | 1

  // 注意:只渲染当前激活面板,保证“单实例切换”
  const content = panels[active];

  const variants = {
    enter: (dir: number) =>
      reduce
        ? { opacity: 0, x: 0, position: "absolute" as const, width: "100%" }
        : {
            opacity: 0,
            x: dir >= 0 ? distance : -distance,
            position: "absolute" as const,
            width: "100%",
          },
    center: {
      opacity: 1,
      x: 0,
      position: "relative" as const,
      width: "100%",
    },
    exit: (dir: number) =>
      reduce
        ? { opacity: 0, x: 0, position: "absolute" as const, width: "100%" }
        : {
            opacity: 0,
            x: dir >= 0 ? -distance : distance,
            position: "absolute" as const,
            width: "100%",
          },
  };

  // a11y:如果提供了 labelledByPrefix,就把 aria-labelledby 指到相应的 Trigger id
  const ariaLabelledBy =
    labelledByPrefix && typeof active !== "object"
      ? `${labelledByPrefix}-${String(active)}`
      : undefined;

  return (
    <div
      className={["relative", className].filter(Boolean).join(" ")}
      // 你也可以传入最小高度,避免高度抖动:style={{ minHeight: 480 }}
    >
      <AnimatePresence initial={false} custom={direction} mode="wait">
        <motion.div
          key={String(active)}
          custom={direction}
          variants={variants}
          initial="enter"
          animate="center"
          exit="exit"
          transition={transition}
          role="tabpanel"
          aria-labelledby={ariaLabelledBy}
          className={panelClassName}
        >
          {content}
        </motion.div>
      </AnimatePresence>
    </div>
  );
}

与现有 Tabs 组合(以 shadcn/ui 为例)

关键点只有两步:
1)Tabs 受控:用本地 tab 状态管理 value/onValueChange
2)用 <AnimatedPanels /> 替换四个 <TabsContent>,并把每个面板内容作为 panels 的值。

"use client";

import { useState } from "react";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { AnimatedPanels } from "./animated-panels"; // ← 就是上面的通用组件
import { Card } from "@/components/ui/card";

const order = ["installation", "usage", "commands", "api"] as const;
type Tab = (typeof order)[number];

export default function Demo() {
  const [tab, setTab] = useState<Tab>("installation");

  const panels = {
    installation: (
      <Card className="p-6">这里是安装面板的内容(你的真实内容)</Card>
    ),
    usage: <Card className="p-6">这里是使用面板的内容</Card>,
    commands: <Card className="p-6">这里是命令面板的内容</Card>,
    api: <Card className="p-6">这里是 API 面板的内容</Card>,
  };

  return (
    <div className="space-y-6">
      <Tabs
        value={tab}
        onValueChange={(v) => setTab(v as Tab)}
        className="space-y-6"
      >
        <TabsList className="grid grid-cols-4">
          <TabsTrigger id="tab-installation" value="installation">
            安装
          </TabsTrigger>
          <TabsTrigger id="tab-usage" value="usage">
            使用
          </TabsTrigger>
          <TabsTrigger id="tab-commands" value="commands">
            命令
          </TabsTrigger>
          <TabsTrigger id="tab-api" value="api">
            API
          </TabsTrigger>
        </TabsList>

        {/* 面板区域:只渲染当前一个面板 */}
        <AnimatedPanels
          active={tab}
          order={order as unknown as (string | number)[]}
          panels={panels}
          distance={48}
          labelledByPrefix="tab"
          className="min-h-[320px]" // 避免高度跳动
        />
      </Tabs>
    </div>
  );
}

想集成到 Radix/Headless Tabs 或自研 Tabs?只要能拿到当前值顺序,填入 <AnimatedPanels active order panels /> 即可。


设计细节与取舍

  1. 为什么是“单实例面板”?
    只渲染当前内容,避免 display: none 抢先把 DOM 干掉,exit 才有机会播放;也减少 DOM 体积与内存占用。

  2. 绝对/相对定位切换

    • 进/出场阶段:position: absolute; width: 100%,便于位移动画不影响布局。
    • 居中阶段:position: relative,恢复正常文档流。
  3. 方向判定
    order.indexOf(active) - order.indexOf(prevActive) 的正负值来判断“向左/向右”。如果两个值相同或首次渲染,方向为 0(只淡入)。

  4. 避免高度跳动
    简单方案:给容器一个 min-height(比如最大期望高度)。
    进阶方案:测量内容高度过渡,但要小心和绝对定位的交互复杂度,通常没必要。

  5. 尊重“减少动效”
    通过 prefers-reduced-motion 自动降级到纯 opacity,遵循可访问性规范。

  6. SSR 安全
    usePrefersReducedMotion 中做了 typeof window === "undefined" 判断,避免服务端渲染报错。


可拓展玩法(改改参数即可)

  • 仅淡入淡出:把 distance 设为 0 或强制 reduce = true
  • 方向相反:将 enter/exit 的符号对调即可。
  • 缩放动画:在 variantsenter/exit 加上 scale: 0.98 → 1
  • 加速/减速:把 transition.type 改为 "tween" 并设置 duration: 0.22

示例(淡入 + 微缩放):

const variants = {
  enter: { opacity: 0, scale: 0.98, position: "absolute" as const, width: "100%" },
  center: { opacity: 1, scale: 1, position: "relative" as const, width: "100%" },
  exit:  { opacity: 0, scale: 0.98, position: "absolute" as const, width: "100%" },
};

性能建议

  • 重内容懒加载:对不常访问的面板做 dynamic(() => import("./HeavyPanel"))
  • 记忆化:面板内容如果依赖昂贵计算,可用 useMemo 缓存。
  • 避免不必要重排:面板内尽量避免布局抖动(图片提前占位等)。

常见坑

  • 直接给 TabsContent 做动画:在很多库里不可行,退出动画不会播。
  • 顺序列表缺失/错误order 必须覆盖所有可能的 active 值;否则方向判断会出错。
  • 首次渲染方向异常:记得在没有 prevActive 时设方向为 0
  • 高度忽闪:容器加 min-height 是最省事的办法。

最终清单(Checklist)

  • [x] Tabs 是受控的(value/onValueChange
  • [x] 使用 <AnimatedPanels active order panels />
  • [x] 容器 position: relative,面板进出用 absolute
  • [x] 处理 prefers-reduced-motion
  • [x] 适当的 min-height
  • [x] a11y:aria-labelledby 指向对应触发器

结语

这套“单实例 + 方向感”的切换模式足够通用:

  • 对任何 Tabs 库友好;
  • 代码量小、耦合度低;
  • 支持 a11y 与减少动效;
  • 可按需扩展(缩放、淡入、滑动、交叉淡入等)。

把上面的 AnimatedPanels 复制到你的项目里,换上各面板的真实内容,你就拥有了一个优雅、可维护、可复用的 Tabs 过渡方案。

个人笔记

可以把AnimatePresencemode设置为"popLayout",这样就可以让淡出与淡入同时进行,视觉更流畅。

应用举例:

"use client";

import { useState } from "react";
import { Copy, CheckCircle, Terminal, Package } from "lucide-react";
import { motion, AnimatePresence } from "motion/react";

import { Button } from "@/components/ui/button";
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Separator } from "@/components/ui/separator";
import { useClipboard } from "use-clipboard-copy";
import { toast } from "sonner";

export default function CLIPage() {
  const clipboard = useClipboard();
  const [copiedStates, setCopiedStates] = useState<{ [key: string]: boolean }>(
    {}
  );

  const handleCopy = (text: string, key: string) => {
    clipboard.copy(text);
    setCopiedStates((prev) => ({ ...prev, [key]: true }));
    setTimeout(() => {
      setCopiedStates((prev) => ({ ...prev, [key]: false }));
    }, 2000);
    toast.success("复制成功!", {
      description: `已复制: ${text}`,
    });
  };

  const CopyButton = ({ text, copyKey }: { text: string; copyKey: string }) => (
    <Button
      variant="ghost"
      size="sm"
      onClick={() => handleCopy(text, copyKey)}
      className="h-6 w-6 p-0"
      aria-label="复制代码"
    >
      {copiedStates[copyKey] ? (
        <CheckCircle className="h-3 w-3 text-green-500" />
      ) : (
        <Copy className="h-3 w-3" />
      )}
    </Button>
  );

  const CodeBlock = ({
    code,
    language = "bash",
    copyKey,
  }: {
    code: string;
    language?: string;
    copyKey: string;
  }) => (
    <div className="relative">
      <pre className="bg-muted/50 rounded-md p-4 pr-12 overflow-x-auto whitespace-pre-wrap break-words">
        <code className={`text-sm font-mono language-${language}`}>{code}</code>
      </pre>
      <div className="absolute top-2 right-2">
        <CopyButton text={code} copyKey={copyKey} />
      </div>
    </div>
  );

  // --- Tabs 受控 + 方向判定 ---
  const tabOrder = ["installation", "usage", "commands", "api"] as const;
  type Tab = (typeof tabOrder)[number];
  const [tab, setTab] = useState<Tab>("installation");
  const [prevTab, setPrevTab] = useState<Tab>("installation");
  const direction =
    Math.sign(tabOrder.indexOf(tab) - tabOrder.indexOf(prevTab)) || 1; // 1:向右,-1:向左

  const panelVariants = {
    enter: (dir: number) => ({
      x: dir * 80, // 新面板从右进(向右切) / 从左进(向左切)
      opacity: 0,
      position: "absolute" as const,
      width: "100%",
    }),
    center: {
      x: 0,
      opacity: 1,
      position: "relative" as const,
      width: "100%",
    },
    exit: (dir: number) => ({
      x: -dir * 80, // 旧面板往左出(向右切) / 往右出(向左切)
      opacity: 0,
      position: "absolute" as const,
      width: "100%",
    }),
  };

  return (
    <div className="container mx-auto px-6 py-8">
      {/* Header */}
      <motion.div
        className="mb-8"
        initial={{ opacity: 0, y: 20 }}
        animate={{ opacity: 1, y: 0 }}
        transition={{
          type: "spring",
          stiffness: 120,
          damping: 40,
          duration: 0.25,
        }}
      >
        <div className="flex items-center gap-3 mb-6">
          <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary">
            <Terminal className="h-6 w-6 text-primary-foreground" />
          </div>
          <div>
            <h1 className="text-4xl font-bold tracking-tight">CLI 工具</h1>
            <p className="text-muted-foreground">
              一键安装和管理 QiuYe UI 组件
            </p>
          </div>
        </div>

        <div className="flex items-center gap-4 text-sm text-muted-foreground">
          <div className="flex items-center gap-2">
            <Package className="h-4 w-4" />
            <span>基于 shadcn/ui CLI</span>
          </div>
          <div className="flex items-center gap-2">
            <span>•</span>
            <span>官方工具支持</span>
          </div>
          <div className="flex items-center gap-2">
            <span>•</span>
            <span>无需额外安装</span>
          </div>
        </div>
      </motion.div>

      {/* Content */}
      <div>
        <Tabs
          value={tab}
          onValueChange={(v) => {
            setPrevTab(tab);
            setTab(v as Tab);
          }}
          className="space-y-6"
        >
          <motion.div
            initial={{ opacity: 0, y: 20 }}
            animate={{ opacity: 1, y: 0 }}
            transition={{
              type: "spring",
              stiffness: 120,
              damping: 40,
              duration: 0.5,
              delay: 0.45,
            }}
          >
            <TabsList className="grid w-full grid-cols-4">
              <TabsTrigger id="tab-installation" value="installation">
                安装
              </TabsTrigger>
              <TabsTrigger id="tab-usage" value="usage">
                使用
              </TabsTrigger>
              <TabsTrigger id="tab-commands" value="commands">
                命令
              </TabsTrigger>
              <TabsTrigger id="tab-api" value="api">
                API
              </TabsTrigger>
            </TabsList>
          </motion.div>

          <motion.div
            initial={{ opacity: 0, y: 20 }}
            animate={{ opacity: 1, y: 0 }}
            transition={{
              type: "spring",
              stiffness: 120,
              damping: 40,
              duration: 0.5,
              delay: 0.75,
            }}
          >
            {/* Animated panels */}
            <div className="relative min-h-[480px]">
              <AnimatePresence initial={false} custom={direction} mode="popLayout">
                <motion.div
                  key={tab}
                  custom={direction}
                  variants={panelVariants}
                  initial="enter"
                  animate="center"
                  exit="exit"
                  transition={{
                    type: "spring",
                    stiffness: 220,
                    damping: 28,
                    mass: 0.8,
                  }}
                  role="tabpanel"
                  aria-labelledby={`tab-${tab}`}
                >
                  {tab === "installation" && (
                    <div className="space-y-6">
                      <Card>
                        <CardHeader>
                          <CardTitle>使用 Shadcn/ui CLI</CardTitle>
                          <CardDescription>
                            使用官方 shadcn/ui CLI 工具安装 QiuYe UI 组件
                          </CardDescription>
                        </CardHeader>
                        <CardContent className="space-y-6">
                          <div>
                            <h3 className="text-lg font-semibold mb-3">
                              前置要求
                            </h3>
                            <p className="text-sm text-muted-foreground mb-3">
                              确保您的项目已经安装并配置了 shadcn/ui:
                            </p>
                            <CodeBlock
                              code="npx shadcn@latest init"
                              copyKey="shadcn-init"
                            />
                            <p className="text-sm text-muted-foreground mt-2">
                              如果还没有初始化 shadcn/ui,请先运行上述命令
                            </p>
                          </div>

                          <Separator />

                          <div>
                            <h3 className="text-lg font-semibold mb-3">
                              方式一:配置注册表(推荐)
                            </h3>
                            <p className="text-sm text-muted-foreground mb-3">
                              在项目根目录的{" "}
                              <code className="text-xs bg-muted px-1 py-0.5 rounded">
                                components.json
                              </code>{" "}
                              文件中添加QiuYe UI的注册表:
                            </p>
                            <CodeBlock
                              code={`{
  "registries": {
    "@qiuye-ui": "https://qiuye-ui.vercel.app/registry/{name}.json"
  }
}`}
                              language="json"
                              copyKey="registry-config"
                            />
                            <p className="text-sm text-muted-foreground mt-3 mb-3">
                              然后使用简化的命令安装组件:
                            </p>
                            <CodeBlock
                              code="npx shadcn@latest add @qiuye-ui/animated-button"
                              copyKey="install-component"
                            />
                          </div>

                          <Separator />

                          <div>
                            <h3 className="text-lg font-semibold mb-3">
                              方式二:直接URL安装
                            </h3>
                            <p className="text-sm text-muted-foreground mb-3">
                              如果不想配置注册表,可以直接使用URL安装组件:
                            </p>
                            <CodeBlock
                              code="npx shadcn@latest add https://qiuye-ui.vercel.app/registry/animated-button.json"
                              copyKey="install-component-url"
                            />
                            <p className="text-sm text-muted-foreground mt-2">
                              直接指定组件的注册表JSON文件URL进行安装
                            </p>
                          </div>
                        </CardContent>
                      </Card>
                    </div>
                  )}

                  {tab === "usage" && (
                    <div className="space-y-6">
                      <Card>
                        <CardHeader>
                          <CardTitle>基本使用</CardTitle>
                          <CardDescription>
                            学习如何在您的项目中使用 QiuYe UI 组件
                          </CardDescription>
                        </CardHeader>
                        <CardContent className="space-y-6">
                          <div>
                            <h3 className="text-lg font-semibold mb-3">
                              1. 安装组件
                            </h3>
                            <p className="text-sm text-muted-foreground mb-3">
                              方式一:使用注册表名称(需先配置注册表)
                            </p>
                            <div className="space-y-3">
                              <CodeBlock
                                code="npx shadcn@latest add @qiuye-ui/animated-button"
                                copyKey="add-single"
                              />
                              <CodeBlock
                                code="npx shadcn@latest add @qiuye-ui/gradient-card @qiuye-ui/typing-text"
                                copyKey="add-multiple"
                              />
                            </div>
                            <p className="text-sm text-muted-foreground mt-3 mb-3">
                              方式二:直接使用URL(无需配置)
                            </p>
                            <div className="space-y-3">
                              <CodeBlock
                                code="npx shadcn@latest add https://qiuye-ui.vercel.app/registry/animated-button.json"
                                copyKey="add-single-url"
                              />
                            </div>
                            <p className="text-sm text-muted-foreground mt-2">
                              支持单个组件安装或批量安装多个组件
                            </p>
                          </div>

                          <div>
                            <h3 className="text-lg font-semibold mb-3">
                              2. 在代码中使用
                            </h3>
                            <CodeBlock
                              code={`import { AnimatedButton } from "@/components/qiuye-ui/animated-button";

export default function App() {
  return (
    <div>
      <AnimatedButton animation="bounce">
        点击我!
      </AnimatedButton>
    </div>
  );
}`}
                              language="tsx"
                              copyKey="usage-example"
                            />
                          </div>

                          <div>
                            <h3 className="text-lg font-semibold mb-3">
                              3. 查看可用组件
                            </h3>
                            <p className="text-sm text-muted-foreground mb-3">
                              访问组件浏览器查看所有可用组件和演示:
                            </p>
                            <CodeBlock
                              code="https://qiuye-ui.vercel.app/components"
                              copyKey="component-browser"
                            />
                          </div>
                        </CardContent>
                      </Card>

                      <Card>
                        <CardHeader>
                          <CardTitle>项目配置</CardTitle>
                          <CardDescription>
                            自定义CLI工具的行为和设置
                          </CardDescription>
                        </CardHeader>
                        <CardContent className="space-y-4">
                          <div>
                            <h4 className="font-semibold mb-2">配置文件示例</h4>
                            <CodeBlock
                              code={`{
  "style": "new-york",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "config": "tailwind.config.js",
    "css": "app/globals.css",
    "baseColor": "slate",
    "cssVariables": true
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "qiuye-ui": "@/components/qiuye-ui"
  }
}`}
                              language="json"
                              copyKey="config-example"
                            />
                          </div>
                        </CardContent>
                      </Card>
                    </div>
                  )}

                  {tab === "commands" && (
                    <div className="space-y-6">
                      <Card>
                        <CardHeader>
                          <CardTitle>常用命令</CardTitle>
                          <CardDescription>
                            使用官方 shadcn/ui CLI 管理 QiuYe UI 组件
                          </CardDescription>
                        </CardHeader>
                        <CardContent className="space-y-6">
                          <div className="grid gap-4">
                            {[
                              {
                                command: "npx shadcn@latest init",
                                description: "初始化 shadcn/ui 项目配置",
                                example: "npx shadcn@latest init",
                              },
                              {
                                command:
                                  "npx shadcn@latest add @qiuye-ui/[component]",
                                description:
                                  "添加 QiuYe UI 组件(需配置注册表)",
                                example:
                                  "npx shadcn@latest add @qiuye-ui/animated-button",
                              },
                              {
                                command: "npx shadcn@latest add [URL]",
                                description: "直接使用URL添加组件",
                                example:
                                  "npx shadcn@latest add https://qiuye-ui.vercel.app/registry/animated-button.json",
                              },
                              {
                                command:
                                  "npx shadcn@latest add @qiuye-ui/[multiple]",
                                description: "批量添加多个组件",
                                example:
                                  "npx shadcn@latest add @qiuye-ui/gradient-card @qiuye-ui/typing-text",
                              },
                              {
                                command: "npx shadcn@latest --help",
                                description: "查看CLI工具帮助信息",
                                example: "npx shadcn@latest --help",
                              },
                            ].map((item, index) => (
                              <div
                                key={index}
                                className="border rounded-lg p-4"
                              >
                                <div className="flex items-start justify-between mb-2">
                                  <div>
                                    <code className="text-sm font-mono bg-muted px-2 py-1 rounded">
                                      {item.command}
                                    </code>
                                    <p className="text-sm text-muted-foreground mt-1">
                                      {item.description}
                                    </p>
                                  </div>
                                  <CopyButton
                                    text={item.example}
                                    copyKey={`cmd-${index}`}
                                  />
                                </div>
                                <div className="bg-muted/30 rounded p-2 text-xs font-mono">
                                  $ {item.example}
                                </div>
                              </div>
                            ))}
                          </div>

                          <Separator />

                          <div>
                            <h3 className="text-lg font-semibold mb-3">
                              可用组件列表
                            </h3>
                            <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
                              {[
                                // TODO: 后续需要添加更多组件, 或支持动态加载组件列表
                                "animated-button",
                                "gradient-card",
                                "typing-text",
                              ].map((component) => (
                                <div
                                  key={component}
                                  className="flex items-center justify-between p-3 bg-muted/30 rounded-lg"
                                >
                                  <code className="text-sm font-mono">
                                    @qiuye-ui/{component}
                                  </code>
                                  <CopyButton
                                    text={`npx shadcn@latest add @qiuye-ui/${component}`}
                                    copyKey={`component-${component}`}
                                  />
                                </div>
                              ))}
                            </div>
                          </div>
                        </CardContent>
                      </Card>
                    </div>
                  )}

                  {tab === "api" && (
                    <div className="space-y-6">
                      <Card>
                        <CardHeader>
                          <CardTitle>注册表结构</CardTitle>
                          <CardDescription>
                            静态注册表文件的结构和访问方式
                          </CardDescription>
                        </CardHeader>
                        <CardContent className="space-y-6">
                          <div className="grid gap-4">
                            {[
                              {
                                endpoint: "/registry/[component].json",
                                description: "单个组件的详细配置和源代码",
                                example:
                                  "https://qiuye-ui.vercel.app/registry/animated-button.json",
                              },
                            ].map((api, index) => (
                              <div
                                key={index}
                                className="border rounded-lg p-4"
                              >
                                <div className="flex items-center gap-3 mb-2">
                                  <Badge variant="secondary">GET</Badge>
                                  <code className="text-sm font-mono">
                                    {api.endpoint}
                                  </code>
                                </div>
                                <p className="text-sm text-muted-foreground mb-2">
                                  {api.description}
                                </p>
                                <div className="bg-muted/30 rounded p-2 text-xs font-mono flex items-center justify-between">
                                  <span>{api.example}</span>
                                  <CopyButton
                                    text={api.example}
                                    copyKey={`endpoint-${index}`}
                                  />
                                </div>
                              </div>
                            ))}
                          </div>

                          <Separator />

                          <div>
                            <h3 className="text-lg font-semibold mb-3">
                              注册表配置
                            </h3>
                            <p className="text-sm text-muted-foreground mb-3">
                              在您的项目中添加以下配置到{" "}
                              <code className="text-xs bg-muted px-1 py-0.5 rounded">
                                components.json
                              </code>
                              :
                            </p>
                            <CodeBlock
                              code={`{
  "registries": {
    "@qiuye-ui": "https://qiuye-ui.vercel.app/registry/{name}.json"
  }
}`}
                              language="json"
                              copyKey="registry-config-final"
                            />
                          </div>

                          <Separator />

                          <div>
                            <h3 className="text-lg font-semibold mb-3">
                              组件文件结构
                            </h3>
                            <p className="text-sm text-muted-foreground mb-3">
                              每个组件的JSON文件包含以下信息:
                            </p>
                            <CodeBlock
                              code={`{
  "name": "component-name",
  "type": "registry:component",
  "dependencies": ["react", "motion"],
  "registryDependencies": [],
  "files": [
    {
      "type": "registry:component",
      "name": "component-name.tsx",
      "content": "组件源代码..."
    }
  ]
}`}
                              language="json"
                              copyKey="component-structure"
                            />
                          </div>
                        </CardContent>
                      </Card>
                    </div>
                  )}
                </motion.div>
              </AnimatePresence>
            </div>
          </motion.div>
        </Tabs>
      </div>
    </div>
  );
}

A Student on the way to full stack of Web3.