{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ruixen-gradient-footer",
  "type": "registry:ui",
  "title": "Ruixen Gradient Footer",
  "description": "A normal-height footer with a blurred rainbow pinned to the bottom of the viewport, stretching up from the floor over the last stretch of scroll. Gradient inspired by Dia Browser. One inline SVG, no dependencies.",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/ruixenui/ruixen-gradient-footer.tsx",
      "content": "\"use client\";\n\n// Ruixen Gradient Footer — a normal footer that sits at the bottom of the page.\n// Its content reads first; the blurred rainbow is pinned to the bottom of the\n// viewport and stretches up from the floor over the last stretch of scroll,\n// hitting full height exactly when you reach the end of the page.\n// One inline <svg> — no canvas, no giant scroll spacer.\n//\n// Gradient design inspired by Dia Browser — https://www.diabrowser.com\n\nimport {\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  type CSSProperties,\n  type ReactNode,\n} from \"react\";\n\ntype Stop = { offset: number; color: string };\n\nconst VBW = 1271;\nconst VBH = 599;\n\n// Ruixen's stops, floor (0) → top (1): dark ember → blue → near-white → yellow\n// → red-orange → magenta → transparent pink.\nconst RUIXEN_STOPS: Stop[] = [\n  { offset: 0, color: \"#340B05\" },\n  { offset: 0.1827, color: \"#0358F7\" },\n  { offset: 0.2837, color: \"#5092C7\" },\n  { offset: 0.4135, color: \"#E1ECFE\" },\n  { offset: 0.5866, color: \"#FFD400\" },\n  { offset: 0.6827, color: \"#FA3D1D\" },\n  { offset: 0.8029, color: \"#FD02F5\" },\n  { offset: 1, color: \"#FFC0FD00\" },\n];\n\n// Height curve: a gentle power falloff, giving the flatter, pyramid-like rise of\n// the original footer (short edges, tallest middle).\nfunction bellHeights(n: number, peak: number, valley: number): number[] {\n  const out: number[] = [];\n  const mid = (n - 1) / 2;\n  for (let i = 0; i < n; i++) {\n    const t = mid === 0 ? 0 : Math.abs(i - mid) / mid; // 0 center → 1 edge\n    const eased = 1 - Math.pow(t, 1.24);\n    out.push(peak * VBH * (valley + (1 - valley) * eased));\n  }\n  return out;\n}\n\nconst clamp01 = (v: number) => Math.max(0, Math.min(1, v));\n\nexport interface RuixenGradientFooterProps {\n  /** Footer content — links, wordmark, copyright — shown above the glow. */\n  children?: ReactNode;\n  /**\n   * Height of the glow band pinned to the viewport bottom. Doubles as the\n   * scroll distance the reveal takes, and the room reserved under the content.\n   */\n  gradientHeight?: string;\n  /**\n   * Resting height of the glow, as a fraction of the band — a thin, flat strip\n   * of rainbow along the bottom edge before the scroll reveal starts. `0` keeps\n   * it hidden until the last screen.\n   */\n  minReveal?: number;\n  /** Number of blurred columns. */\n  bars?: number;\n  /** Blur in viewBox units. */\n  blur?: number;\n  /** Peak height as a fraction of the viewBox. */\n  peak?: number;\n  /** Edge height as a fraction of the peak (0..1). */\n  valley?: number;\n  /** Vertical rainbow gradient stops, floor (0) → top (1). */\n  stops?: Stop[];\n  className?: string;\n  style?: CSSProperties;\n}\n\nexport function RuixenGradientFooter({\n  children,\n  gradientHeight = \"65vh\",\n  minReveal = 0.045,\n  bars = 9,\n  blur = 15,\n  peak = 0.98,\n  valley = 0.55,\n  stops = RUIXEN_STOPS,\n  className,\n  style,\n}: RuixenGradientFooterProps) {\n  const uid = useId().replace(/:/g, \"\");\n  const bandRef = useRef<HTMLDivElement>(null);\n  // minReveal = a flat strip on the floor, 1 = risen to full height.\n  const [progress, setProgress] = useState(minReveal);\n\n  useEffect(() => {\n    const el = bandRef.current;\n    if (!el) return;\n    // Bind to the element's OWN window so this tracks the right scroll context\n    // on a real page and inside the docs preview iframe alike.\n    const doc = el.ownerDocument;\n    const win = doc.defaultView ?? window;\n    const measure = () => {\n      // offsetHeight ignores the transform, so the band can measure itself.\n      const h = el.offsetHeight || 1;\n      // How much scroll is left before the end of the page. The glow starts\n      // rising once that's within its own height, and is full at the bottom.\n      const left =\n        doc.documentElement.scrollHeight - win.innerHeight - win.scrollY;\n      const t = clamp01((h - left) / h);\n      setProgress(minReveal + (1 - minReveal) * t);\n    };\n    measure();\n    win.addEventListener(\"scroll\", measure, { passive: true });\n    win.addEventListener(\"resize\", measure, { passive: true });\n    return () => {\n      win.removeEventListener(\"scroll\", measure);\n      win.removeEventListener(\"resize\", measure);\n    };\n  }, [minReveal]);\n\n  const colW = VBW / bars;\n\n  return (\n    // The glow is pinned to the viewport, so the footer reserves the same\n    // height beneath its content for the glow to land in.\n    <footer\n      className={className}\n      style={{ paddingBottom: gradientHeight, ...style }}\n    >\n      {children}\n\n      {/* ponytail: fixed to the viewport — a transformed/filtered ancestor\n          would capture it. Give the footer a plain containing block. */}\n      <div\n        ref={bandRef}\n        aria-hidden\n        style={{\n          position: \"fixed\",\n          left: 0,\n          right: 0,\n          bottom: 0,\n          height: gradientHeight,\n          pointerEvents: \"none\",\n          transformOrigin: \"bottom\",\n          transform: `scaleY(${progress})`,\n          willChange: \"transform\",\n        }}\n      >\n        <svg\n          style={{ height: \"100%\", width: \"100%\", display: \"block\" }}\n          viewBox={`0 0 ${VBW} ${VBH}`}\n          preserveAspectRatio=\"none\"\n          fill=\"none\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n        >\n          <defs>\n            <linearGradient id={`grad-${uid}`} x1=\"0\" y1=\"1\" x2=\"0\" y2=\"0\">\n              {stops.map((s, i) => (\n                <stop key={i} offset={s.offset} stopColor={s.color} />\n              ))}\n            </linearGradient>\n            <filter\n              id={`blur-${uid}`}\n              x=\"-50%\"\n              y=\"-50%\"\n              width=\"200%\"\n              height=\"200%\"\n            >\n              <feGaussianBlur stdDeviation={blur} />\n            </filter>\n          </defs>\n          {bellHeights(bars, peak, valley).map((barH, i) => (\n            <g key={i} filter={`url(#blur-${uid})`}>\n              <rect\n                x={i * colW}\n                y={VBH - barH}\n                width={colW * 1.23}\n                height={barH}\n                fill={`url(#grad-${uid})`}\n              />\n            </g>\n          ))}\n        </svg>\n      </div>\n    </footer>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ruixen/ruixen-gradient-footer.tsx"
    }
  ]
}