{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "code-stack",
  "title": "代码文件堆叠",
  "description": "带行号与语法骨架的代码文件卡，支持扇形、级联、网格排列与悬停散开。",
  "dependencies": [
    "class-variance-authority"
  ],
  "registryDependencies": [
    "https://yiui.dev/r/flow-tone.json"
  ],
  "files": [
    {
      "path": "src/registry/ui/code-stack/code-stack.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cva } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\nimport { flowToneStyle, type FlowTone } from \"@/lib/flow-tone\"\n\nconst LANG_TONES: Record<string, FlowTone> = {\n  ts: \"blue\",\n  tsx: \"cyan\",\n  js: \"amber\",\n  json: \"primary\",\n  css: \"violet\",\n  md: \"muted\",\n  py: \"green\",\n  go: \"cyan\",\n  rs: \"rose\",\n  yaml: \"amber\",\n}\n\n/** 文档调整覆盖项；不提供 Provider 时沿用组件属性。 */\ntype CodeStackReport = {\n  spread: CodeStackSpread\n  hoverSpread: boolean\n  max: number\n  active: boolean\n}\ntype CodeStackTuning = Partial<CodeStackReport> & {\n  /** 固定调整期间的画框。@default false */\n  stableFrame?: boolean\n  /** 回报覆盖前的示例取值。 */\n  onReport?: (report: CodeStackReport) => void\n}\nconst CodeStackTuningContext = React.createContext<CodeStackTuning>({})\n\ntype CodeStackSpread = \"fan\" | \"cascade\" | \"grid\"\ntype CodeStackProps = React.ComponentProps<\"div\"> & {\n  /** 排列方式。@default \"fan\" */\n  spread?: CodeStackSpread\n  /** 悬停或内部控件聚焦时散开。@default true */\n  hoverSpread?: boolean\n  /** 最多显示的文件数，0 显示空堆叠。@default 8 */\n  max?: number\n}\ntype CodeFileLine = {\n  indent?: number\n  tokens: { width: number; tone?: FlowTone }[]\n}\ntype CodeFileProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** 语言缩写，未知语言使用主题强调色。@default \"ts\" */\n  lang?: string\n  name?: string\n  /** 每行的缩进与骨架片段，width 是相对于行宽的百分比。 */\n  lines?: CodeFileLine[]\n  /** 自动骨架的确定性种子。@default 1 */\n  seed?: number\n  active?: boolean\n}\n\nconst codeStackVariants = cva(\n  \"relative isolate w-full [container-type:inline-size]\",\n  {\n    variants: {\n      spread: {\n        fan: \"h-80\",\n        cascade: \"h-80\",\n        grid: \"grid grid-cols-2 gap-4 p-4 sm:grid-cols-4\",\n      },\n    },\n    defaultVariants: { spread: \"fan\" },\n  },\n)\n\n/** 纯 CSS 排列，不为装饰性悬停增加键盘焦点。 */\nfunction CodeStack({\n  className,\n  children,\n  spread: spreadProp = \"fan\",\n  hoverSpread: hoverSpreadProp = true,\n  max: maxProp = 8,\n  ...props\n}: CodeStackProps) {\n  const tuning = React.useContext(CodeStackTuningContext)\n  const { onReport } = tuning\n  const spread = tuning.spread ?? spreadProp\n  const hoverSpread = tuning.hoverSpread ?? hoverSpreadProp\n  const max = tuning.max ?? maxProp\n  React.useEffect(() => {\n    onReport?.({\n      spread: spreadProp,\n      hoverSpread: hoverSpreadProp,\n      max: maxProp,\n      active: React.Children.toArray(children).some(\n        (child) =>\n          React.isValidElement<CodeFileProps>(child) &&\n          Boolean(child.props.active),\n      ),\n    })\n  }, [onReport, spreadProp, hoverSpreadProp, maxProp, children])\n\n  const frame = React.useRef<HTMLDivElement>(null)\n  React.useLayoutEffect(() => {\n    const element = frame.current\n    if (!element || !tuning.stableFrame) return\n    // 保留示例原始高度；网格溢出时在画框内滚动，不挤压卡片。\n    element.style.height = element.getBoundingClientRect().height + \"px\"\n    return () => {\n      element.style.height = \"\"\n    }\n  }, [tuning.stableFrame])\n  const files = React.Children.toArray(children).slice(\n    0,\n    Math.max(0, Math.floor(max)),\n  )\n  return (\n    <div\n      ref={frame}\n      data-slot=\"code-stack\"\n      data-spread={spread}\n      data-hover-spread={hoverSpread || undefined}\n      className={cn(\n        codeStackVariants({ spread }),\n        tuning.stableFrame && \"content-start overflow-auto\",\n        className,\n      )}\n      {...props}\n    >\n      <style href=\"code-stack-layout\" precedence=\"default\">{`\n        [data-slot=\"code-stack\"] > [data-slot=\"code-stack-position\"] { transition: transform 280ms cubic-bezier(.2,.8,.2,1); }\n        [data-slot=\"code-stack\"][data-spread=\"fan\"] > [data-slot=\"code-stack-position\"] {\n          position: absolute; top: 50%; left: 50%; width: clamp(6.5rem, 25cqw, 10rem);\n          transform: translate(calc(-50% + var(--code-offset) * min(7cqw, 2.5rem)), calc(-48% + var(--code-curve) * 4px)) rotate(calc(var(--code-offset) * 9deg));\n        }\n        [data-slot=\"code-stack\"][data-spread=\"cascade\"] > [data-slot=\"code-stack-position\"] {\n          position: absolute; top: 50%; left: 50%; width: clamp(7rem, 30cqw, 11rem);\n          transform: translate(calc(-50% + var(--code-offset) * min(7cqw, 2.2rem)), calc(-50% + var(--code-offset) * 20px));\n        }\n        @media (hover: hover) and (prefers-reduced-motion: no-preference) {\n          [data-slot=\"code-stack\"][data-hover-spread][data-spread=\"fan\"]:hover > [data-slot=\"code-stack-position\"],\n          [data-slot=\"code-stack\"][data-hover-spread][data-spread=\"fan\"]:focus-within > [data-slot=\"code-stack-position\"] {\n            transform: translate(calc(-50% + var(--code-offset) * min(9cqw, 3.2rem)), calc(-55% + var(--code-curve) * 5px)) rotate(calc(var(--code-offset) * 13deg));\n          }\n          [data-slot=\"code-stack\"][data-hover-spread][data-spread=\"cascade\"]:hover > [data-slot=\"code-stack-position\"],\n          [data-slot=\"code-stack\"][data-hover-spread][data-spread=\"cascade\"]:focus-within > [data-slot=\"code-stack-position\"] {\n            transform: translate(calc(-50% + var(--code-offset) * min(10cqw, 3rem)), calc(-50% + var(--code-offset) * 28px));\n          }\n        }\n        @media (prefers-reduced-motion: reduce) {\n          [data-slot=\"code-stack\"] > [data-slot=\"code-stack-position\"] { transition: none; }\n        }\n      `}</style>\n      {files.map((file, index) => {\n        const offset = index - (files.length - 1) / 2\n        return (\n          <div\n            key={React.isValidElement(file) ? (file.key ?? index) : index}\n            data-slot=\"code-stack-position\"\n            className=\"relative min-w-0 has-data-active:z-20\"\n            style={\n              {\n                \"--code-offset\": offset,\n                \"--code-curve\": offset * offset,\n              } as React.CSSProperties\n            }\n          >\n            {file}\n          </div>\n        )\n      })}\n    </div>\n  )\n}\n\nfunction skeleton(seed: number): CodeFileLine[] {\n  let value = seed >>> 0\n  const random = () => {\n    value = (Math.imul(value, 1664525) + 1013904223) >>> 0\n    return value / 4294967296\n  }\n  return Array.from({ length: 8 }, (_, index) => ({\n    indent: index === 0 || index === 7 ? 0 : 8,\n    tokens: Array.from(\n      { length: 2 + Math.floor(random() * 2) },\n      (_, token) => ({\n        width: 10 + Math.floor(random() * 16),\n        tone: token === 0 ? \"muted\" : token === 1 ? undefined : \"green\",\n      }),\n    ),\n  }))\n}\n\n/** 代码的示意卡片；骨架不是实际源码，文件名仍然能被读屏读取。 */\nfunction CodeFile({\n  className,\n  lang = \"ts\",\n  name,\n  lines,\n  seed = 1,\n  active = false,\n  style,\n  ...props\n}: CodeFileProps) {\n  const tuning = React.useContext(CodeStackTuningContext)\n  active = tuning.active ?? active\n  const language = lang.toLowerCase()\n  const rows = lines ?? skeleton(seed)\n  return (\n    <div\n      data-slot=\"code-file\"\n      data-active={active || undefined}\n      className={cn(\n        \"relative flex aspect-[4/5] w-full min-w-0 flex-col overflow-hidden rounded-lg border border-edge bg-card text-card-foreground shadow-sm data-active:border-foreground/30 data-active:bg-muted\",\n        className,\n      )}\n      style={{ ...flowToneStyle(LANG_TONES[language] ?? \"primary\"), ...style }}\n      {...props}\n    >\n      <div\n        data-slot=\"code-file-header\"\n        className=\"flex min-w-0 flex-wrap items-center gap-1 border-b border-edge px-3 py-2.5\"\n      >\n        <span\n          aria-hidden=\"true\"\n          className=\"size-1.5 shrink-0 rounded-full bg-(--flow-rose)\"\n        />\n        <span\n          aria-hidden=\"true\"\n          className=\"size-1.5 shrink-0 rounded-full bg-(--flow-amber)\"\n        />\n        <span\n          aria-hidden=\"true\"\n          className=\"size-1.5 shrink-0 rounded-full bg-(--flow-green)\"\n        />\n        <span\n          className=\"mt-1 basis-full truncate font-mono text-[9px] text-muted-foreground\"\n          title={name}\n        >\n          {name ?? `file.${lang}`}\n        </span>\n      </div>\n      <div\n        data-slot=\"code-file-lines\"\n        aria-hidden=\"true\"\n        className=\"flex min-h-0 flex-1 flex-col justify-center-safe gap-2 overflow-hidden px-3 py-3\"\n      >\n        {rows.map((line, index) => (\n          <div key={index} className=\"flex h-2 shrink-0 items-center gap-2\">\n            <span className=\"w-2 shrink-0 text-right font-mono text-[8px] text-muted-foreground\">\n              {index + 1}\n            </span>\n            <div\n              className=\"flex min-w-0 flex-1 items-center gap-1\"\n              style={{\n                paddingLeft: `${Math.max(0, Math.min(60, line.indent ?? 0))}%`,\n              }}\n            >\n              {line.tokens.map((token, part) => (\n                <span\n                  key={part}\n                  className=\"h-1 shrink-0 rounded-full bg-(--flow-tone)/45\"\n                  style={{\n                    width: `${Math.max(0, Math.min(100, token.width))}%`,\n                    ...(token.tone ? flowToneStyle(token.tone) : {}),\n                  }}\n                />\n              ))}\n            </div>\n          </div>\n        ))}\n      </div>\n      <div\n        data-slot=\"code-file-footer\"\n        className=\"flex items-end justify-between gap-2 px-3 pb-3\"\n      >\n        <span\n          aria-hidden=\"true\"\n          className=\"font-mono text-[10px] text-muted-foreground/60\"\n        >\n          {\"</>\"}\n        </span>\n        <span className=\"rounded-md border border-edge bg-(--flow-tone)/10 px-1.5 py-0.5 font-mono text-[10px] font-semibold text-(--flow-tone)\">\n          {language.toUpperCase()}\n        </span>\n      </div>\n    </div>\n  )\n}\n\nexport { CodeStackTuningContext, CodeStack, CodeFile, codeStackVariants }\nexport type {\n  CodeStackTuning,\n  CodeStackReport,\n  CodeStackProps,\n  CodeStackSpread,\n  CodeFileProps,\n  CodeFileLine,\n}\n",
      "type": "registry:ui",
      "target": "@ui/code-stack/code-stack.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"
}
