{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "progressive-blur",
  "title": "Progressive Blur",
  "description": "A blur that ramps from 0 to 20px over 80px toward an edge, from stacked masked backdrop layers, for headers, bottom bars and composers. Optionally scroll-linked so it appears only once content is underneath.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://eased.dev/r/eased-tokens.json",
    "https://eased.dev/r/eased-scroll-fade.json"
  ],
  "files": [
    {
      "path": "src/registry/eased/progressive-blur/progressive-blur.tsx",
      "content": "\"use client\";\n\nimport { motion, useMotionValue, type MotionValue } from \"motion/react\";\nimport { useEffect, useLayoutEffect, useState, type CSSProperties, type RefObject } from \"react\";\nimport { BlurStack, observeScrollContent, type BlurSide } from \"../_primitives/scroll-fade/scroll-fade\";\n\nexport type ProgressiveBlurProps = {\n  /** The edge the blur is strongest at; it sits flush against that edge of its positioned parent. Default \"top\". */\n  side?: BlurSide;\n  /** How far the ramp reaches in from the edge, px. Default 80. */\n  size?: number;\n  /** Blur at the edge, px. Default 20. */\n  maxBlur?: number;\n  /** Stacked layers; more is smoother and costs more. Default 6. */\n  layers?: number;\n  /** Lay a surface-coloured scrim under the ramp so text over busy content stays legible. Default true. */\n  tint?: boolean;\n  /**\n   * Scroll-linked strength: the blur fades in as content scrolls under the edge and is gone\n   * when nothing is there (top: scrolled to the start; bottom: scrolled to the end). A ref or\n   * the element itself; a ref whose scroller mounts later links on the next render, and an\n   * element held in state (from a callback ref) links as soon as it exists.\n   */\n  scrollRef?: RefObject<HTMLElement | null> | HTMLElement | null;\n  /** Scroll distance (px) over which a scroll-linked blur reaches full strength. Default `size`. */\n  scrollRange?: number;\n  /** Manual strength, 0–1, when there is no `scrollRef`. Default 1. */\n  strength?: number | MotionValue<number>;\n  /** Position and stacking, e.g. `z-10`. Do not set opacity, filter or mask here: they blank the blur. */\n  className?: string;\n};\n\nconst PLACE: Record<BlurSide, string> = {\n  top: \"inset-x-0 top-0\",\n  bottom: \"inset-x-0 bottom-0\",\n  left: \"inset-y-0 left-0\",\n  right: \"inset-y-0 right-0\",\n};\n\nconst TINT: Record<BlurSide, string> = {\n  top: \"bg-linear-to-b\",\n  bottom: \"bg-linear-to-t\",\n  left: \"bg-linear-to-r\",\n  right: \"bg-linear-to-l\",\n};\n\n/**\n * Progressive Blur: a blur that ramps from nothing to `maxBlur` over `size` px toward an edge,\n * built from stacked masked backdrop layers, for headers, bottom bars and composers. Linked to a\n * scroller, it only appears once there is content underneath to blur.\n */\nexport function ProgressiveBlur({\n  side = \"top\",\n  size = 80,\n  maxBlur = 20,\n  layers = 6,\n  tint = true,\n  scrollRef,\n  scrollRange,\n  strength = 1,\n  className = \"\",\n}: ProgressiveBlurProps) {\n  const linked = useMotionValue(scrollRef ? 0 : 1);\n  const range = scrollRange ?? size;\n\n  // Resolve the scroller after commit, so a ref filled by a later-mounting scroller still links.\n  const [el, setEl] = useState<HTMLElement | null>(null);\n  // No deps on purpose: a ref's `.current` changes without a render to key on.\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  useLayoutEffect(() => {\n    const next = scrollRef instanceof HTMLElement ? scrollRef : (scrollRef?.current ?? null);\n    if (next !== el) setEl(next);\n  });\n\n  useEffect(() => {\n    if (!el) return;\n    const update = () => {\n      const vertical = side === \"top\" || side === \"bottom\";\n      const pos = vertical ? el.scrollTop : Math.abs(el.scrollLeft);\n      const max = vertical ? el.scrollHeight - el.clientHeight : el.scrollWidth - el.clientWidth;\n      const under = side === \"top\" || side === \"left\" ? pos : max - pos;\n      linked.set(Math.min(1, Math.max(0, under / range)));\n    };\n    update();\n    el.addEventListener(\"scroll\", update, { passive: true });\n    const unobserve = observeScrollContent(el, update);\n    return () => {\n      el.removeEventListener(\"scroll\", update);\n      unobserve();\n    };\n  }, [el, side, range, linked]);\n\n  const level = scrollRef ? linked : strength;\n  const extent: CSSProperties = side === \"top\" || side === \"bottom\" ? { height: size } : { width: size };\n\n  return (\n    <div aria-hidden className={`pointer-events-none absolute ${PLACE[side]} ${className}`} style={extent}>\n      {tint && <motion.div className={`absolute inset-0 ${TINT[side]} from-surface/75 via-surface/30 to-transparent`} style={{ opacity: level }} />}\n      <BlurStack side={side} layers={layers} maxBlur={maxBlur} strength={level} />\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@components/eased/progressive-blur/progressive-blur.tsx"
    }
  ],
  "docs": "Import ProgressiveBlur from components/eased/progressive-blur/progressive-blur. Place it over the scroller in a positioned parent, never inside it, and keep opacity, filter and mask off its ancestors.",
  "type": "registry:ui"
}