{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "haze",
  "type": "registry:ui",
  "title": "Haze",
  "description": "A progressive blur edge — stacked backdrop-filter layers with overlapping mask gradients ramp content from sharp to fully blurred through a container or viewport edge, with an optional tint wash. The iOS-style depth idiom for scrolling lists, floating toolbars, and hero bottoms. Pure CSS, zero JS at runtime.",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/utility/haze/haze.tsx",
      "type": "registry:ui",
      "target": "components/crucible/haze.tsx",
      "content": "// No \"use client\" directive on purpose: Haze is a pure, static JSX composition —\n// no hooks, no event handlers, no browser APIs — so it stays server-compatible\n// and adds zero JS to the client bundle when rendered from an RSC.\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface HazeProps extends Omit<React.ComponentPropsWithoutRef<\"div\">, \"children\"> {\n  /** Which edge the haze clings to — content sharpens away from this edge. @default \"bottom\" */\n  edge?: \"top\" | \"bottom\" | \"left\" | \"right\";\n  /**\n   * Size of the blur zone in pixels, measured inward from the edge. Clamped to\n   * 16–480 — backdrop-filter cost scales with covered area, so shallow zones\n   * are both the look and the budget.\n   * @default 128\n   */\n  depth?: number;\n  /** Blur ceiling in pixels reached at the deepest point of the zone. Clamped to 0.5–96. @default 24 */\n  blur?: number;\n  /**\n   * Number of stacked backdrop layers building the ramp. More layers = a finer\n   * blur gradient (and more compositing work). Clamped to 3–12; the default is\n   * tuned so no banding seam shows at any DPR.\n   * @default 6\n   */\n  layers?: number;\n  /**\n   * Easing exponent applied to the ramp's gradient stops. `1` distributes the\n   * ramp evenly through the zone; `> 1` holds sharpness longer before a steeper\n   * dive at the edge; `< 1` starts hazing sooner. Clamped to 0.25–4.\n   * @default 1\n   */\n  curve?: number;\n  /**\n   * Tint color washed toward the edge on top of the blur — any CSS color.\n   * The default is a breath of near-black for dark UIs; pass a brand hue for\n   * a color wash.\n   * @default \"#050508\"\n   */\n  tint?: string;\n  /** Tint opacity (0–1) at the deepest point of the zone. `0` removes the tint layer entirely. @default 0.2 */\n  tintStrength?: number;\n  /**\n   * `\"absolute\"` pins the haze inside the nearest positioned ancestor (give\n   * the container `position: relative`); `\"fixed\"` pins it to the viewport\n   * edge — the classic floating-toolbar / bottom-of-page treatment.\n   * @default \"absolute\"\n   */\n  position?: \"absolute\" | \"fixed\";\n  /** Let pointer events pass through to the content beneath the haze. @default true */\n  passThrough?: boolean;\n}\n\nconst EDGE_PLACEMENT: Record<NonNullable<HazeProps[\"edge\"]>, 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\n/** Gradient axis runs from the sharp boundary of the zone toward the blurred edge. */\nconst EDGE_DIRECTION: Record<NonNullable<HazeProps[\"edge\"]>, string> = {\n  top: \"to top\",\n  bottom: \"to bottom\",\n  left: \"to left\",\n  right: \"to right\",\n};\n\nconst clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));\n\n/**\n * Haze — a progressive blur edge: a zone where content ramps from sharp to\n * fully blurred through a smooth gradient, the iOS/visionOS depth idiom. Use\n * it over scrolling lists, under floating toolbars, or across a hero image's\n * bottom edge.\n *\n * Built as stacked `backdrop-filter` layers on a geometric blur ladder, each\n * masked by a staggered gradient that overlaps its neighbors — adjacent layers\n * crossfade through shared segments, so the blur *ramps* continuously instead\n * of stepping, with no banding seam at any DPR. An optional tint layer washes\n * a color toward the edge.\n *\n * Zero JS at runtime — a static CSS composition. Nothing animates, so there is\n * no reduced-motion concern (the effect is spatial, not temporal) and no\n * reduced-motion hook is consumed. Decorative: `aria-hidden`, pointer-events\n * pass through by default.\n *\n * Performance: `backdrop-filter` cost scales with the area it covers — keep\n * zones shallow (the `depth` clamp enforces a sane ceiling) and avoid tiling\n * many deep hazes in one viewport.\n */\nexport function Haze({\n  edge = \"bottom\",\n  depth = 128,\n  blur = 24,\n  layers = 6,\n  curve = 1,\n  tint = \"#050508\",\n  tintStrength = 0.2,\n  position = \"absolute\",\n  passThrough = true,\n  className,\n  style,\n  ...props\n}: HazeProps) {\n  const depthPx = clamp(depth, 16, 480);\n  const ceiling = clamp(blur, 0.5, 96);\n  const count = Math.round(clamp(layers, 3, 12));\n  const exponent = clamp(curve, 0.25, 4);\n  const strength = clamp(tintStrength, 0, 1);\n\n  const direction = EDGE_DIRECTION[edge];\n  const vertical = edge === \"top\" || edge === \"bottom\";\n\n  // The zone is divided into `count + 1` segments. Layer i is fully opaque\n  // across segments [i, i+1] and fades out across the neighboring segment on\n  // each side — so every point in the zone sits inside at least one opaque\n  // band while adjacent layers crossfade through the shared segments. The\n  // crossfade overlap is the anti-banding mechanism: effective blur\n  // interpolates between rungs instead of stepping.\n  const segments = count + 1;\n  const stop = (t: number) =>\n    `${(Math.pow(clamp(t / segments, 0, 1), exponent) * 100).toFixed(2)}%`;\n\n  const blurLayers = Array.from({ length: count }, (_, i) => {\n    // Geometric ladder (each rung doubles toward the ceiling): blur perception\n    // is roughly logarithmic, so equal ratio steps read as equal visual steps.\n    // With the defaults (6 layers, 24px) the rungs run 0.75 → 1.5 → 3 → 6 →\n    // 12 → 24px — the first rung is sub-pixel, so the zone's onset is\n    // seamless too.\n    const blurPx = ceiling / 2 ** (count - 1 - i);\n    const maskImage =\n      i === 0\n        ? // First rung: opaque from the zone start (its blur is imperceptible).\n          `linear-gradient(${direction}, #000 0%, #000 ${stop(1)}, transparent ${stop(2)})`\n        : i === count - 1\n          ? // Last rung: holds opaque through 100% so the ceiling blur reaches\n            // the very edge instead of falling off.\n            `linear-gradient(${direction}, transparent ${stop(i - 1)}, #000 ${stop(i)}, #000 100%)`\n          : `linear-gradient(${direction}, transparent ${stop(i - 1)}, #000 ${stop(i)}, #000 ${stop(i + 1)}, transparent ${stop(i + 2)})`;\n    return (\n      <div\n        key={i}\n        className=\"absolute inset-0\"\n        style={{\n          backdropFilter: `blur(${blurPx}px)`,\n          WebkitBackdropFilter: `blur(${blurPx}px)`,\n          maskImage,\n          WebkitMaskImage: maskImage,\n        }}\n      />\n    );\n  });\n\n  return (\n    <div\n      aria-hidden\n      data-crucible=\"haze\"\n      className={cn(\n        position === \"fixed\" ? \"fixed\" : \"absolute\",\n        EDGE_PLACEMENT[edge],\n        passThrough && \"pointer-events-none\",\n        className\n      )}\n      style={{\n        ...(vertical ? { height: depthPx } : { width: depthPx }),\n        ...style,\n      }}\n      {...props}\n    >\n      {blurLayers}\n      {strength > 0 && (\n        <div\n          className=\"absolute inset-0\"\n          style={{\n            // Eased three-stop ramp: a straight two-stop alpha gradient can\n            // band on wide zones; the soft midpoint keeps the wash smooth.\n            background: `linear-gradient(${direction}, transparent 0%, color-mix(in srgb, ${tint} ${(strength * 35).toFixed(1)}%, transparent) 50%, color-mix(in srgb, ${tint} ${(strength * 100).toFixed(1)}%, transparent) 100%)`,\n          }}\n        />\n      )}\n    </div>\n  );\n}\n"
    }
  ]
}