{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sula-frame",
  "type": "registry:ui",
  "dependencies": [
    "clsx",
    "motion",
    "ogl",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "https://usva.build/r/sula-motion.json",
    "https://usva.build/r/sula-core.json"
  ],
  "files": [
    {
      "path": "frame-geometry.ts",
      "target": "components/ui/frame-geometry.ts",
      "type": "registry:ui",
      "content": "import { clamp01, easeOutCubic, smoothstep } from \"./curves\";\n\n/** Band width in CSS px when the consumer passes nothing. */\nexport const DEFAULT_THICKNESS = 2;\n/** Corner radius for a fixed viewport frame when none is given, before the\n * width scale. */\nexport const DEFAULT_RADIUS = 16;\n\n/** CSS-px radius of the disc that goos toward the cursor. */\nexport const BLOB_RADIUS = 26;\n/** CSS-px smooth-min radius merging the pointer disc into the band. Wide enough\n * that the neck reads as surface tension, not a butt-join. */\nexport const BLOB_K = 42;\n\n/** Idle edge undulation, always on while the frame is visible. */\nexport const WOBBLE_REST = 0.6;\n/** Extra edge displacement added at full hover/focus energy. */\nexport const WOBBLE_ENERGY = 3.0;\n\n/** Within this CSS distance of the band the pointer goo is at full strength. */\nexport const EDGE_NEAR = 8;\n/** Past this CSS distance the pointer goo has faded out entirely. */\nexport const EDGE_FAR = 120;\n\n/** Per-frame easing of the pointer position and the energy scalar. */\nexport const POINTER_EASE = 0.16;\nexport const ENERGY_EASE = 0.09;\n/** Perimeter highlight travel, in turns per second. */\nexport const SWEEP_SPEED = 0.12;\n/** Duration of the one-time intro reveal, in seconds. */\nexport const INTRO_DELAY_SECONDS = 0.1;\nexport const INTRO_SECONDS = 4;\n\n/** A rounded-box ring in CSS space: centre, half-extents, corner radius. */\nexport interface Ring {\n  cx: number;\n  cy: number;\n  hx: number;\n  hy: number;\n  r: number;\n}\n\nexport interface IntroFrame {\n  progress: number;\n  radius: number;\n}\n\n/** Geometry for the one-time edge-to-frame flow. The shell front itself is\n * resolved in the shader; the CPU grows the corners on the same eased clock. */\nexport function introFrame(ring: Ring, progress: number): IntroFrame {\n  const eased = easeOutCubic(clamp01(progress));\n  return {\n    progress: eased,\n    radius: ring.r * (0.18 + 0.82 * eased),\n  };\n}\n\n/** The same ring flattened to device px with Y flipped, ready for the shader. */\nexport interface PackedRing {\n  center: [number, number];\n  half: [number, number];\n  radius: number;\n}\n\n/**\n * Signed distance to a rounded box (Inigo Quilez, MIT). Negative inside, zero on\n * the edge, positive outside. Ported to JS so the pointer gate and the tests can\n * reason about the same edge the shader draws.\n */\nexport function sdRoundBox(\n  px: number,\n  py: number,\n  bx: number,\n  by: number,\n  r: number,\n): number {\n  const qx = Math.abs(px) - bx + r;\n  const qy = Math.abs(py) - by + r;\n  const outside = Math.hypot(Math.max(qx, 0), Math.max(qy, 0));\n  return Math.min(Math.max(qx, qy), 0) + outside - r;\n}\n\n/**\n * Corner radius for a fixed viewport frame: a gentle scale with width, clamped so\n * a phone still rounds and an ultrawide does not turn into a stadium. Mirrors the\n * frame radius the source LiquidBorder used.\n */\nexport function fixedRadius(width: number): number {\n  return Math.min(Math.max(38, width * 0.02), 78);\n}\n\n/**\n * Resolves the corner radius from, in order: an explicit prop, the wrapped box's\n * computed border-radius, or a default (width-scaled in fixed mode). Never\n * negative.\n */\nexport function resolveRadius(options: {\n  explicit?: number;\n  computed?: number;\n  fixed: boolean;\n  width: number;\n}): number {\n  const { explicit, computed, fixed, width } = options;\n  if (typeof explicit === \"number\") return Math.max(0, explicit);\n  if (fixed) return fixedRadius(width);\n  if (typeof computed === \"number\") return Math.max(0, computed);\n  return DEFAULT_RADIUS;\n}\n\n/**\n * The ring inscribed in a canvas of the given CSS size, pulled in by `inset` and\n * with the radius clamped to what the half-extents can hold.\n */\nexport function frameRing(box: {\n  width: number;\n  height: number;\n  inset: number;\n  radius: number;\n}): Ring {\n  const hx = Math.max(box.width / 2 - box.inset, 1);\n  const hy = Math.max(box.height / 2 - box.inset, 1);\n  return {\n    cx: box.width / 2,\n    cy: box.height / 2,\n    hx,\n    hy,\n    r: Math.max(0, Math.min(box.radius, hx, hy)),\n  };\n}\n\n/** Flattens a CSS-space ring to device px, flipping Y for `gl_FragCoord`. */\nexport function packRing(\n  ring: Ring,\n  dpr: number,\n  canvasHeight: number,\n): PackedRing {\n  return {\n    center: [ring.cx * dpr, (canvasHeight - ring.cy) * dpr],\n    half: [ring.hx * dpr, ring.hy * dpr],\n    radius: ring.r * dpr,\n  };\n}\n\n/**\n * How hard the pointer goos the nearest edge: `presence` (0 away, 1 hovering)\n * gated by proximity to the band, so a cursor drifting through the middle of a\n * wrapped card never raises a blob, and one grazing the edge raises a full one.\n */\nexport function pointerStrength(\n  px: number,\n  py: number,\n  ring: Ring,\n  presence: number,\n): number {\n  const dist = Math.abs(\n    sdRoundBox(px - ring.cx, py - ring.cy, ring.hx, ring.hy, ring.r),\n  );\n  const gate = 1 - smoothstep(EDGE_NEAR, EDGE_FAR, dist);\n  return clamp01(presence) * gate;\n}\n\n/** One pointer disc as a flat vec4 (x, y, radius, strength) in device px, Y flipped. */\nexport function packBlob(\n  px: number,\n  py: number,\n  radiusCss: number,\n  strength: number,\n  dpr: number,\n  canvasHeight: number,\n): [number, number, number, number] {\n  return [px * dpr, (canvasHeight - py) * dpr, radiusCss * dpr, strength];\n}\n\n/** Idle-plus-energy edge amplitude in CSS px for a given energy scalar. */\nexport function wobbleFor(energy: number): number {\n  return WOBBLE_REST + WOBBLE_ENERGY * clamp01(energy);\n}\n"
    },
    {
      "path": "sula-frame.tsx",
      "target": "components/ui/sula-frame.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\nimport { useReducedMotion } from \"motion/react\";\nimport * as React from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"@/lib/utils\";\nimport { createBorderField } from \"./border\";\nimport {\n  type FieldColors,\n  liftTint,\n  resolveColor,\n  shineForBackdrop,\n} from \"./field\";\nimport { useContextRecovery } from \"./recovery\";\nimport { clamp01 } from \"./curves\";\nimport {\n  BLOB_K,\n  BLOB_RADIUS,\n  DEFAULT_RADIUS,\n  DEFAULT_THICKNESS,\n  ENERGY_EASE,\n  frameRing,\n  INTRO_DELAY_SECONDS,\n  INTRO_SECONDS,\n  introFrame,\n  POINTER_EASE,\n  packBlob,\n  packRing,\n  pointerStrength,\n  resolveRadius,\n  SWEEP_SPEED,\n  wobbleFor,\n} from \"./frame-geometry\";\n\nexport interface SulaFrameProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** false wraps its own box; true is a position:fixed viewport frame. */\n  fixed?: boolean;\n  /** Corner radius in px. Wrapper mode defaults to the box's computed\n   * border-radius; fixed mode to a width scale. */\n  radius?: number;\n  /** Band width in px. Defaults to 2. */\n  thickness?: number;\n  /** Gap between the frame and the edge in px. Defaults to 0. */\n  inset?: number;\n  /** false mounts no canvas; reduced motion paints the static border. */\n  fluid?: boolean;\n  /** One-time reveal ramp on mount. Skipped under reduced motion. Defaults true. */\n  intro?: boolean;\n  accentColor?: string;\n  backdrop?: string;\n  tint?: string;\n  shine?: number;\n  children?: React.ReactNode;\n}\n\nconst MAX_DPR = 2;\n\nfunction readColors(\n  node: HTMLElement,\n  overrides: {\n    backdrop?: string;\n    tint?: string;\n    accent?: string;\n    shine?: number;\n  },\n): FieldColors {\n  const styles = getComputedStyle(node);\n  const token = (name: string) => styles.getPropertyValue(name).trim();\n  const backdrop = resolveColor(overrides.backdrop ?? token(\"--usva-bg\"));\n  const tintToken =\n    overrides.tint ?? token(\"--usva-surface-2\") ?? token(\"--usva-surface\");\n  const base: FieldColors = {\n    backdrop,\n    tint: resolveColor(tintToken || token(\"--usva-surface\")),\n    accent: resolveColor(overrides.accent ?? token(\"--usva-accent\")),\n    shine: overrides.shine ?? shineForBackdrop(backdrop),\n  };\n  return { ...base, tint: liftTint(base.tint, base.accent) };\n}\n\n/** Parses the top-left corner radius of an element, in px. */\nfunction computedRadius(node: HTMLElement): number {\n  const value = Number.parseFloat(getComputedStyle(node).borderTopLeftRadius);\n  return Number.isFinite(value) ? value : 0;\n}\n\nexport const SulaFrame = React.forwardRef<HTMLDivElement, SulaFrameProps>(\n  (\n    {\n      fixed = false,\n      radius,\n      thickness = DEFAULT_THICKNESS,\n      inset = 0,\n      fluid = true,\n      intro = true,\n      accentColor,\n      backdrop,\n      tint,\n      shine,\n      className,\n      style,\n      children,\n      ...props\n    },\n    forwardedRef,\n  ) => {\n    const reduced = useReducedMotion();\n    const containerRef = React.useRef<HTMLDivElement | null>(null);\n    const layerRef = React.useRef<HTMLDivElement | null>(null);\n    const canvasRef = React.useRef<HTMLCanvasElement | null>(null);\n\n    const thicknessRef = React.useRef(thickness);\n    thicknessRef.current = thickness;\n    const insetRef = React.useRef(inset);\n    insetRef.current = inset;\n    const radiusRef = React.useRef(radius);\n    radiusRef.current = radius;\n    const introRef = React.useRef(intro);\n    introRef.current = intro;\n\n    const overrides = React.useMemo(\n      () => ({ backdrop, tint, accent: accentColor, shine }),\n      [backdrop, tint, accentColor, shine],\n    );\n    const overridesRef = React.useRef(overrides);\n    overridesRef.current = overrides;\n    const fieldRef = React.useRef<ReturnType<typeof createBorderField>>(null);\n\n    const { failed, generation, onContextLost, onContextReady } =\n      useContextRecovery(canvasRef);\n    const [mounted, setMounted] = React.useState(false);\n    React.useEffect(() => setMounted(true), []);\n\n    const animated = fluid && !reduced && !failed && mounted;\n    const staticBorder = mounted && (!fluid || reduced || failed);\n    const keepCanvas = fluid && !reduced && mounted;\n\n    // biome-ignore lint/correctness/useExhaustiveDependencies: `generation` is not read here, it is what rebuilds the field on a restored context\n    React.useEffect(() => {\n      if (!animated) return;\n      const canvas = canvasRef.current;\n      const layer = layerRef.current;\n      const container = containerRef.current;\n      const measureNode = fixed ? layer : container;\n      if (!canvas || !layer || !measureNode) return;\n\n      const field = createBorderField({\n        canvas,\n        colors: readColors(measureNode, overridesRef.current),\n        onContextLost,\n      });\n      if (!field) {\n        onContextLost();\n        return;\n      }\n      fieldRef.current = field;\n      onContextReady();\n      const refreshColors = () =>\n        field.setColors(readColors(measureNode, overridesRef.current));\n\n      let width = 0;\n      let height = 0;\n      let dpr = 1;\n      const measure = (): boolean => {\n        const w = fixed\n          ? window.innerWidth\n          : Math.ceil(measureNode.getBoundingClientRect().width);\n        const h = fixed\n          ? window.innerHeight\n          : Math.ceil(measureNode.getBoundingClientRect().height);\n        if (w <= 0 || h <= 0) return false;\n        const nextDpr = Math.min(window.devicePixelRatio || 1, MAX_DPR);\n        if (w !== width || h !== height || nextDpr !== dpr) {\n          canvas.style.width = `${w}px`;\n          canvas.style.height = `${h}px`;\n          field.resize(w, h, nextDpr);\n        }\n        width = w;\n        height = h;\n        dpr = nextDpr;\n        return true;\n      };\n\n      const pointer = { x: -1e5, y: -1e5 };\n      let pointerTarget = 0;\n      let presence = 0;\n      let focusTarget = 0;\n      let focus = 0;\n\n      const ringFor = () =>\n        frameRing({\n          width,\n          height,\n          inset: insetRef.current,\n          radius: resolveRadius({\n            explicit: radiusRef.current,\n            computed: fixed\n              ? undefined\n              : computedRadius(measureNode as HTMLElement),\n            fixed,\n            width,\n          }),\n        });\n\n      const draw = (elapsed: number) => {\n        presence += (pointerTarget - presence) * POINTER_EASE;\n        focus += (focusTarget - focus) * ENERGY_EASE;\n\n        const introActive = introRef.current;\n        const introT = introActive\n          ? clamp01((elapsed - INTRO_DELAY_SECONDS) / INTRO_SECONDS)\n          : 1;\n\n        const energy = clamp01(Math.max(presence, focus));\n        const ring = ringFor();\n        const introGeometry = introFrame(ring, introT);\n        const packed = packRing(\n          { ...ring, r: introGeometry.radius },\n          dpr,\n          height,\n        );\n\n        const strength = pointerStrength(pointer.x, pointer.y, ring, presence);\n        const blobs = new Array<number>(2 * 4).fill(0);\n        let blobCount = 0;\n        if (strength > 0.01) {\n          const b = packBlob(\n            pointer.x,\n            pointer.y,\n            BLOB_RADIUS,\n            strength,\n            dpr,\n            height,\n          );\n          blobs[0] = b[0];\n          blobs[1] = b[1];\n          blobs[2] = b[2];\n          blobs[3] = b[3];\n          blobCount = 1;\n        }\n\n        field.draw({\n          center: packed.center,\n          half: packed.half,\n          radius: packed.radius,\n          thickness: (thicknessRef.current / 2) * dpr,\n          wobble: wobbleFor(Math.max(energy, 1 - introGeometry.progress)) * dpr,\n          energy,\n          sweep: (elapsed * SWEEP_SPEED) % 1,\n          time: elapsed,\n          blobs,\n          blobCount,\n          blobK: BLOB_K * dpr,\n          intro: introGeometry.progress,\n        });\n      };\n\n      if (!measure()) return;\n\n      let raf = 0;\n      let elapsed = 0;\n      let last = performance.now();\n      let killed = false;\n      const tick = () => {\n        if (killed) return;\n        const now = performance.now();\n        elapsed += (now - last) / 1000;\n        last = now;\n        draw(elapsed);\n        raf = requestAnimationFrame(tick);\n      };\n      const stop = () => cancelAnimationFrame(raf);\n      const run = () => {\n        last = performance.now();\n        stop();\n        raf = requestAnimationFrame(tick);\n      };\n\n      const pointerHost = fixed ? window : container;\n      const onMove = (event: Event) => {\n        const e = event as PointerEvent;\n        if (fixed) {\n          pointer.x = e.clientX;\n          pointer.y = e.clientY;\n        } else {\n          const box = (container as HTMLElement).getBoundingClientRect();\n          pointer.x = e.clientX - box.left;\n          pointer.y = e.clientY - box.top;\n        }\n        pointerTarget = 1;\n      };\n      const onLeave = () => {\n        pointerTarget = 0;\n      };\n      pointerHost?.addEventListener(\"pointermove\", onMove as EventListener);\n      pointerHost?.addEventListener(\"pointerleave\", onLeave);\n\n      const onFocusIn = (event: FocusEvent) => {\n        const target = event.target as HTMLElement | null;\n        if (target?.matches?.(\":focus-visible\")) focusTarget = 1;\n      };\n      const onFocusOut = () => {\n        if (!container?.querySelector(\":focus-visible\")) focusTarget = 0;\n      };\n      if (!fixed) {\n        container?.addEventListener(\"focusin\", onFocusIn);\n        container?.addEventListener(\"focusout\", onFocusOut);\n      }\n\n      let visible = true;\n      const io =\n        typeof IntersectionObserver === \"undefined\"\n          ? null\n          : new IntersectionObserver((entries) => {\n              visible = entries[0]?.isIntersecting ?? true;\n              if (visible && !document.hidden) run();\n              else stop();\n            });\n      io?.observe(measureNode);\n\n      const onVisibility = () => {\n        if (document.hidden || !visible) stop();\n        else run();\n      };\n      document.addEventListener(\"visibilitychange\", onVisibility);\n\n      const ro =\n        typeof ResizeObserver === \"undefined\"\n          ? null\n          : new ResizeObserver(() => measure());\n      if (fixed) window.addEventListener(\"resize\", measure);\n      else ro?.observe(measureNode);\n\n      const themeObserver =\n        typeof MutationObserver === \"undefined\"\n          ? null\n          : new MutationObserver(() => refreshColors());\n      themeObserver?.observe(document.documentElement, {\n        attributes: true,\n        attributeFilter: [\"data-theme\", \"class\"],\n      });\n\n      run();\n\n      return () => {\n        killed = true;\n        pointerHost?.removeEventListener(\n          \"pointermove\",\n          onMove as EventListener,\n        );\n        pointerHost?.removeEventListener(\"pointerleave\", onLeave);\n        container?.removeEventListener(\"focusin\", onFocusIn);\n        container?.removeEventListener(\"focusout\", onFocusOut);\n        io?.disconnect();\n        document.removeEventListener(\"visibilitychange\", onVisibility);\n        window.removeEventListener(\"resize\", measure);\n        ro?.disconnect();\n        themeObserver?.disconnect();\n        stop();\n        field.dispose();\n        fieldRef.current = null;\n        canvas.style.width = \"\";\n        canvas.style.height = \"\";\n      };\n    }, [animated, fixed, generation]);\n\n    React.useEffect(() => {\n      const measureNode = fixed ? layerRef.current : containerRef.current;\n      if (!measureNode) return;\n      fieldRef.current?.setColors(readColors(measureNode, overrides));\n    }, [overrides, fixed]);\n\n    const staticRingStyle: React.CSSProperties = {\n      position: fixed ? \"fixed\" : \"absolute\",\n      inset: `${inset}px`,\n      borderRadius: fixed ? `${radius ?? DEFAULT_RADIUS}px` : \"inherit\",\n      border: `${thickness}px solid color-mix(in oklab, var(--usva-accent) 55%, transparent)`,\n      boxShadow:\n        \"0 0 24px color-mix(in oklab, var(--usva-accent) 22%, transparent)\",\n    };\n\n    const layer = keepCanvas ? (\n      <div\n        ref={layerRef}\n        aria-hidden=\"true\"\n        className={cn(\n          fixed\n            ? \"pointer-events-none fixed inset-0 z-[2147483647]\"\n            : \"pointer-events-none absolute inset-0 z-10\",\n          !animated && \"hidden\",\n        )}\n      >\n        <canvas ref={canvasRef} className=\"block h-full w-full\" />\n      </div>\n    ) : null;\n\n    const ring = staticBorder ? (\n      <div\n        aria-hidden=\"true\"\n        className={cn(\"pointer-events-none\", fixed ? \"z-[2147483647]\" : \"z-10\")}\n        style={staticRingStyle}\n      />\n    ) : null;\n\n    if (fixed) {\n      return (\n        <div\n          ref={(node) => {\n            containerRef.current = node;\n            if (typeof forwardedRef === \"function\") forwardedRef(node);\n            else if (forwardedRef) forwardedRef.current = node;\n          }}\n          data-fluid={animated ? \"on\" : \"off\"}\n          style={{ display: \"contents\" }}\n          {...props}\n        >\n          {layer ? createPortal(layer, document.body) : null}\n          {ring ? createPortal(ring, document.body) : null}\n          {children}\n        </div>\n      );\n    }\n\n    return (\n      <div\n        ref={(node) => {\n          containerRef.current = node;\n          if (typeof forwardedRef === \"function\") forwardedRef(node);\n          else if (forwardedRef) forwardedRef.current = node;\n        }}\n        data-fluid={animated ? \"on\" : \"off\"}\n        className={cn(\"relative isolate\", className)}\n        style={style}\n        {...props}\n      >\n        {layer}\n        {ring}\n        {children}\n      </div>\n    );\n  },\n);\nSulaFrame.displayName = \"SulaFrame\";\n"
    }
  ]
}