{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ink-bleed-text",
  "title": "Ink bleed text",
  "description": "Ink bleed text is a production-ready text component from BJORK UI, packaged for shadcn registries.",
  "dependencies": [
    "clsx",
    "framer-motion",
    "next-themes",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "components/bjork-ui/text/ink-bleed-text.tsx",
      "content": "\"use client\";\n\n// Ink Bleed Text: a word that soaks into paper. A few drops land, wick outward through the\n// fibres (radius grows with the square root of time, as it does on real paper), and the\n// letters inside are displaced and feathered by turbulence until the ink settles and the\n// type sharpens to crisp. Hovering re-wets the paper around the pointer, and it dries again\n// when the pointer leaves. SVG filters only: feTurbulence + feDisplacementMap + a soft mask.\n//\n// Safari note: WebKit rasterises SVG filters at 1x and repaints them more slowly, so the\n// wet frames are a little softer there. Filters and masks are removed once the ink is dry,\n// so the settled type is plain vector text in every browser.\n\nimport { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from \"react\";\nimport { useReducedMotion } from \"framer-motion\";\nimport { cn } from \"@/lib/utils\";\nimport { BJORK_PALETTE, type BjorkTone } from \"../_core/palette\";\nimport { useBjorkTone } from \"../_core/tone\";\nimport { useVisibleLoop } from \"../_core/loop\";\nimport { cubicBezier } from \"../_core/motion\";\nimport { mulberry32, hashString } from \"../_core/random\";\n\nexport type InkBleedTrigger = \"mount\" | \"in-view\" | \"loop\";\n\nexport interface InkBleedTextProps {\n  text?: string;\n  /** Ink colour. Defaults to the tone's text colour. */\n  color?: string;\n  /** Colour of the wet halo that runs ahead of the ink and fades as it dries. */\n  bleedColor?: string;\n  /** mount: bleed on mount. in-view: when first scrolled into view. loop: bleed, hold, fade, repeat. */\n  trigger?: InkBleedTrigger;\n  /** Seconds from the first drop to dry, crisp type. */\n  duration?: number;\n  /** Seconds before the first drop. */\n  delay?: number;\n  /** Number of drops the ink spreads from, 1 to 6. */\n  drops?: number;\n  /** Seconds the dry word holds in loop mode. */\n  hold?: number;\n  /** How far the wet ink wanders, 0 to 2. */\n  spread?: number;\n  /** Hovering re-wets the paper around the pointer. */\n  rewet?: boolean;\n  /** Largest font size in CSS px. The word shrinks to fit narrower containers. */\n  fontSize?: number;\n  fontWeight?: number;\n  fontFamily?: string;\n  /** Tracking in em. */\n  letterSpacing?: number;\n  tone?: BjorkTone;\n  autoplay?: boolean;\n  /** Renders a still frame this far (0 to 1) through the bleed. For previews. */\n  freezeAt?: number;\n  seed?: number;\n  ariaLabel?: string;\n  className?: string;\n}\n\nconst FS = 100;\nconst subscribeNoop = () => () => {};\nconst getTrue = () => true;\nconst getFalse = () => false;\nconst DEFAULT_FONT = \"var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif\";\nconst settle = cubicBezier(0.3, 0.1, 0.25, 1);\nconst clamp01 = (v: number) => (v < 0 ? 0 : v > 1 ? 1 : v);\n\ninterface Layout {\n  vb: [number, number, number, number];\n  box: [number, number, number, number];\n}\ninterface DropSpec {\n  x: number;\n  y: number;\n  at: number; // start, as a fraction of the duration\n  reach: number; // final radius in user units\n}\n\n// Filter parameters for a wetness `w` (1 = soaking, 0 = dry).\nfunction wetParams(w: number, spread: number) {\n  return {\n    scale: FS * 0.17 * spread * Math.pow(w, 1.4),\n    blur: FS * 0.045 * spread * w * w + 0.001,\n    fiber: 0.95 * Math.pow(w, 1.1),\n    steep: 1 + 6 * Math.pow(w, 0.8),\n    halo: 0.9 * Math.pow(w, 1.3),\n  };\n}\n\ninterface FilterNodes {\n  disp: SVGFEDisplacementMapElement | null;\n  blur: SVGFEGaussianBlurElement | null;\n  haloBlur: SVGFEGaussianBlurElement | null;\n  fib: SVGFECompositeElement | null;\n  haloFib: SVGFECompositeElement | null;\n  steep: SVGFEFuncAElement | null;\n  haloA: SVGFEFuncAElement | null;\n  turb: SVGFETurbulenceElement | null;\n}\nconst emptyNodes = (): FilterNodes => ({\n  disp: null,\n  blur: null,\n  haloBlur: null,\n  fib: null,\n  haloFib: null,\n  steep: null,\n  haloA: null,\n  turb: null,\n});\n\nfunction applyWet(n: FilterNodes, w: number, spread: number) {\n  const p = wetParams(w, spread);\n  n.disp?.setAttribute(\"scale\", p.scale.toFixed(2));\n  n.blur?.setAttribute(\"stdDeviation\", p.blur.toFixed(3));\n  n.haloBlur?.setAttribute(\"stdDeviation\", (p.blur * 3 + FS * 0.02 * w).toFixed(3));\n  n.fib?.setAttribute(\"k3\", p.fiber.toFixed(3));\n  n.fib?.setAttribute(\"k4\", (-p.fiber * 0.5).toFixed(3));\n  n.haloFib?.setAttribute(\"k3\", (p.fiber * 1.3).toFixed(3));\n  n.haloFib?.setAttribute(\"k4\", (-p.fiber * 0.65).toFixed(3));\n  n.steep?.setAttribute(\"slope\", p.steep.toFixed(3));\n  n.steep?.setAttribute(\"intercept\", (0.5 - p.steep / 2).toFixed(3));\n  n.haloA?.setAttribute(\"slope\", (p.halo * 0.95).toFixed(3));\n  n.haloA?.setAttribute(\"intercept\", (-p.halo * 0.08).toFixed(3));\n}\n\nexport function InkBleedText({\n  text = \"Wet ink\",\n  color,\n  bleedColor,\n  trigger = \"in-view\",\n  duration = 2.8,\n  delay = 0.1,\n  drops = 3,\n  hold = 2.6,\n  spread = 1,\n  rewet = true,\n  fontSize = 168,\n  fontWeight = 640,\n  fontFamily = DEFAULT_FONT,\n  letterSpacing = -0.04,\n  tone,\n  autoplay = true,\n  freezeAt,\n  seed,\n  ariaLabel,\n  className,\n}: InkBleedTextProps) {\n  const resolvedTone = useBjorkTone(tone);\n  const palette = BJORK_PALETTE[resolvedTone];\n  const ink = color ?? palette.text;\n  const halo = bleedColor ?? palette.accentInk;\n  // Gate on mount so the server render and hydration agree; the preference applies right after.\n  const mounted = useSyncExternalStore(subscribeNoop, getTrue, getFalse);\n  const reduced = (useReducedMotion() ?? false) && mounted;\n  const frozen = freezeAt !== undefined && !reduced;\n  const animated = !reduced && !frozen;\n  const sp = Math.max(0, Math.min(2, spread));\n  const baseSeed = seed ?? hashString(text);\n\n  const uid = useId().replace(/[^a-zA-Z0-9_-]/g, \"\");\n  const ids = {\n    text: `ibt-text-${uid}`,\n    bleed: `ibt-bleed-${uid}`,\n    wet: `ibt-wet-${uid}`,\n    fibre: `ibt-fibre-${uid}`,\n    reveal: `ibt-reveal-${uid}`,\n    wetIn: `ibt-wetin-${uid}`,\n    wetOut: `ibt-wetout-${uid}`,\n    drop: `ibt-drop-${uid}`,\n    dropOut: `ibt-dropout-${uid}`,\n  };\n\n  const rootRef = useRef<HTMLDivElement>(null);\n  const textRef = useRef<SVGTextElement>(null);\n  const measureRef = useRef<SVGTextElement>(null);\n  const revealGroup = useRef<SVGGElement>(null);\n  const bleedGroup = useRef<SVGGElement>(null);\n  const crispGroup = useRef<SVGGElement>(null);\n  const wetGroup = useRef<SVGGElement>(null);\n  const dropEls = useRef<(SVGCircleElement | null)[]>([]);\n  const wetCircles = useRef<(SVGCircleElement | null)[]>([]);\n  const bleedNodes = useRef<FilterNodes>(emptyNodes());\n  const wetNodes = useRef<FilterNodes>(emptyNodes());\n  const [layout, setLayout] = useState<Layout | null>(null);\n\n  useLayoutEffect(() => {\n    const el = measureRef.current;\n    if (!el) return;\n    let alive = true;\n    const measure = () => {\n      if (!alive) return;\n      let b: DOMRect;\n      try {\n        b = el.getBBox();\n      } catch {\n        return;\n      }\n      if (!b.width) return;\n      const pad = FS * 0.14 * Math.max(0.6, sp);\n      setLayout({ vb: [b.x - pad, b.y - pad, b.width + pad * 2, b.height + pad * 2], box: [b.x, b.y, b.width, b.height] });\n    };\n    measure();\n    document.fonts?.ready.then(measure);\n    return () => {\n      alive = false;\n    };\n  }, [text, fontFamily, fontWeight, letterSpacing, sp]);\n\n  const nDrops = Math.max(1, Math.min(6, Math.round(drops)));\n  const dropSpecs = useMemo<DropSpec[]>(() => {\n    if (!layout) return [];\n    const [bx, by, bw, bh] = layout.box;\n    const rand = mulberry32(baseSeed);\n    const out: DropSpec[] = [];\n    const order = Array.from({ length: nDrops }, (_, i) => i).sort(() => rand() - 0.5);\n    for (let i = 0; i < nDrops; i++) {\n      const x = bx + bw * ((i + 0.5) / nDrops) + (rand() - 0.5) * (bw / nDrops) * 0.5;\n      const y = by + bh * (0.35 + rand() * 0.3);\n      // Reach the farthest corner of this drop's share of the word, with margin for the fibres.\n      const share = bw / nDrops;\n      const reach = Math.hypot(share * 1.05, bh * 0.75) + FS * 0.2;\n      out.push({ x, y, at: (order.indexOf(i) / Math.max(1, nDrops)) * 0.22, reach });\n    }\n    return out;\n  }, [layout, nDrops, baseSeed]);\n\n  // One frame of the bleed at progress `p` (0..1 through `duration`). Returns true once dry.\n  const paintBleed = useCallback(\n    (p: number) => {\n      const dry = p >= 1;\n      const w = 1 - settle(clamp01(p));\n      if (dry) {\n        bleedGroup.current?.removeAttribute(\"filter\");\n        revealGroup.current?.removeAttribute(\"mask\");\n      } else {\n        bleedGroup.current?.setAttribute(\"filter\", `url(#${ids.bleed})`);\n        revealGroup.current?.setAttribute(\"mask\", `url(#${ids.reveal})`);\n        applyWet(bleedNodes.current, w, sp);\n        dropSpecs.forEach((d, i) => {\n          // Lucas-Washburn: wicking distance grows with the square root of time.\n          const local = clamp01((p - d.at) / 0.7);\n          const r = d.reach * Math.sqrt(local);\n          dropEls.current[i]?.setAttribute(\"r\", Math.max(0.01, r).toFixed(2));\n        });\n      }\n      return dry;\n    },\n    [dropSpecs, ids.bleed, ids.reveal, sp],\n  );\n\n  const paintWet = useCallback(\n    (w: number, x: number, y: number) => {\n      const on = w > 0.004;\n      if (!on) {\n        wetGroup.current?.setAttribute(\"display\", \"none\");\n        crispGroup.current?.removeAttribute(\"mask\");\n        return;\n      }\n      wetGroup.current?.removeAttribute(\"display\");\n      crispGroup.current?.setAttribute(\"mask\", `url(#${ids.wetOut})`);\n      applyWet(wetNodes.current, w * 0.6, sp);\n      const r = FS * (0.42 + 0.22 * w);\n      for (const c of wetCircles.current) {\n        if (!c) continue;\n        c.setAttribute(\"cx\", x.toFixed(2));\n        c.setAttribute(\"cy\", y.toFixed(2));\n        c.setAttribute(\"r\", r.toFixed(2));\n      }\n    },\n    [ids.wetOut, sp],\n  );\n\n  const sim = useRef({\n    started: false,\n    t: 0,\n    cycle: 0,\n    wet: 0,\n    wetTarget: 0,\n    px: 0,\n    py: 0,\n    tx: 0,\n    ty: 0,\n    fade: 1,\n  });\n\n  const frame = useCallback(\n    (dt: number) => {\n      const s = sim.current;\n      if (!layout) return false;\n      let busy = false;\n      if (s.started) {\n        s.t += dt;\n        const p = (s.t - delay) / duration;\n        let dry: boolean;\n        if (trigger === \"loop\") {\n          const end = 1 + hold / duration;\n          const fadeLen = 0.6 / duration;\n          if (p > end + fadeLen) {\n            // Next sheet: new drops pattern from a fresh seed.\n            s.t = 0;\n            s.cycle += 1;\n            const seedNext = String(baseSeed + s.cycle * 7919);\n            bleedNodes.current.turb?.setAttribute(\"seed\", seedNext);\n          }\n          const fadeP = clamp01((p - end) / fadeLen);\n          const op = p < 0 ? 0 : 1 - fadeP;\n          if (Math.abs(op - s.fade) > 0.001) {\n            s.fade = op;\n            if (revealGroup.current) revealGroup.current.style.opacity = op.toFixed(3);\n          }\n          dry = paintBleed(Math.max(0, p));\n          busy = true;\n        } else {\n          dry = paintBleed(Math.max(0, p));\n          busy = !dry;\n        }\n        if (p < 0 && revealGroup.current) revealGroup.current.style.opacity = \"0\";\n        else if (trigger !== \"loop\" && revealGroup.current) revealGroup.current.style.opacity = \"1\";\n        void dry;\n      }\n      // Re-wet: wetness rises fast under the pointer and dries slowly.\n      const rate = s.wetTarget > s.wet ? 5 : 1.1;\n      s.wet += (s.wetTarget - s.wet) * (1 - Math.exp(-dt * rate));\n      if (s.wet < 0.004 && s.wetTarget === 0) s.wet = 0;\n      const follow = 1 - Math.exp(-dt * 9);\n      s.px += (s.tx - s.px) * follow;\n      s.py += (s.ty - s.py) * follow;\n      paintWet(s.wet, s.px, s.py);\n      if (s.wet > 0 || s.wetTarget > 0) busy = true;\n      return busy;\n    },\n    [layout, delay, duration, trigger, hold, baseSeed, paintBleed, paintWet],\n  );\n\n  const { wake } = useVisibleLoop(rootRef, frame, { enabled: animated && autoplay && !!layout });\n\n  // Grab the filter primitives the frame loop writes to.\n  useLayoutEffect(() => {\n    if (!layout) return;\n    const svg = rootRef.current?.querySelector(\"svg\");\n    const grab = (id: string): FilterNodes => {\n      const f = svg?.querySelector(`#${id}`);\n      const q = <T extends Element>(k: string) => (f?.querySelector(`[data-k=\"${k}\"]`) as T | null) ?? null;\n      return {\n        disp: q<SVGFEDisplacementMapElement>(\"disp\"),\n        blur: q<SVGFEGaussianBlurElement>(\"blur\"),\n        haloBlur: q<SVGFEGaussianBlurElement>(\"haloBlur\"),\n        fib: q<SVGFECompositeElement>(\"fib\"),\n        haloFib: q<SVGFECompositeElement>(\"haloFib\"),\n        steep: q<SVGFEFuncAElement>(\"steep\"),\n        haloA: q<SVGFEFuncAElement>(\"haloA\"),\n        turb: q<SVGFETurbulenceElement>(\"turb\"),\n      };\n    };\n    bleedNodes.current = grab(ids.bleed);\n    wetNodes.current = grab(ids.wet);\n  }, [layout, ids.bleed, ids.wet]);\n\n  // Initial / still frames.\n  useLayoutEffect(() => {\n    if (!layout) return;\n    if (reduced) {\n      paintBleed(1);\n      paintWet(0, 0, 0);\n      return;\n    }\n    if (frozen) {\n      paintBleed(clamp01(freezeAt ?? 0));\n      paintWet(0, 0, 0);\n      return;\n    }\n    if (!sim.current.started) {\n      paintBleed(0);\n      paintWet(0, 0, 0);\n      if (revealGroup.current) revealGroup.current.style.opacity = \"0\";\n    }\n  }, [layout, reduced, frozen, freezeAt, paintBleed, paintWet]);\n\n  // Start.\n  useEffect(() => {\n    if (!animated || !layout) return;\n    const s = sim.current;\n    if (s.started) return;\n    if (trigger !== \"in-view\") {\n      s.started = true;\n      wake();\n      return;\n    }\n    const el = rootRef.current;\n    if (!el) return;\n    const io = new IntersectionObserver(\n      ([entry]) => {\n        if (entry.isIntersecting) {\n          s.started = true;\n          io.disconnect();\n          wake();\n        }\n      },\n      { threshold: 0.35 },\n    );\n    io.observe(el);\n    return () => io.disconnect();\n  }, [animated, layout, trigger, wake]);\n\n  // Pointer re-wet.\n  useEffect(() => {\n    if (!animated || !rewet || !layout) return;\n    const el = rootRef.current;\n    const svg = el?.querySelector(\"svg\");\n    if (!el || !svg) return;\n    const s = sim.current;\n    const toUser = (e: PointerEvent) => {\n      const r = svg.getBoundingClientRect();\n      return {\n        x: layout.vb[0] + ((e.clientX - r.left) / Math.max(1, r.width)) * layout.vb[2],\n        y: layout.vb[1] + ((e.clientY - r.top) / Math.max(1, r.height)) * layout.vb[3],\n      };\n    };\n    const onEnter = (e: PointerEvent) => {\n      const u = toUser(e);\n      if (s.wet < 0.01) {\n        s.px = u.x;\n        s.py = u.y;\n      }\n      s.tx = u.x;\n      s.ty = u.y;\n      s.wetTarget = 1;\n      wake();\n    };\n    const onMove = (e: PointerEvent) => {\n      const u = toUser(e);\n      s.tx = u.x;\n      s.ty = u.y;\n      s.wetTarget = 1;\n      wake();\n    };\n    const onLeave = () => {\n      s.wetTarget = 0;\n      wake();\n    };\n    svg.addEventListener(\"pointerenter\", onEnter);\n    svg.addEventListener(\"pointermove\", onMove);\n    svg.addEventListener(\"pointerleave\", onLeave);\n    return () => {\n      svg.removeEventListener(\"pointerenter\", onEnter);\n      svg.removeEventListener(\"pointermove\", onMove);\n      svg.removeEventListener(\"pointerleave\", onLeave);\n    };\n  }, [animated, rewet, layout, wake]);\n\n  const vb = layout?.vb;\n  const maxWidth = vb ? (vb[2] * fontSize) / FS : undefined;\n  const region = vb ? { x: vb[0], y: vb[1], width: vb[2], height: vb[3] } : undefined;\n\n  const renderFilter = (id: string, seedOffset: number) => (\n    <filter id={id} filterUnits=\"userSpaceOnUse\" {...region} colorInterpolationFilters=\"sRGB\">\n      <feTurbulence\n        data-k=\"turb\"\n        type=\"fractalNoise\"\n        baseFrequency={0.026}\n        numOctaves={3}\n        seed={baseSeed + seedOffset}\n        result=\"warp\"\n      />\n      <feDisplacementMap\n        data-k=\"disp\"\n        in=\"SourceGraphic\"\n        in2=\"warp\"\n        scale={0}\n        xChannelSelector=\"R\"\n        yChannelSelector=\"G\"\n        result=\"disp\"\n      />\n      <feTurbulence type=\"fractalNoise\" baseFrequency=\"0.11 0.19\" numOctaves={2} seed={baseSeed + seedOffset + 1} result=\"fibre\" />\n      <feGaussianBlur data-k=\"blur\" in=\"disp\" stdDeviation={0} result=\"soft\" />\n      <feComposite data-k=\"fib\" in=\"soft\" in2=\"fibre\" operator=\"arithmetic\" k1={0} k2={1} k3={0} k4={0} result=\"fibrous\" />\n      <feComponentTransfer in=\"fibrous\" result=\"core\">\n        <feFuncA data-k=\"steep\" type=\"linear\" slope={1} intercept={0} />\n      </feComponentTransfer>\n      <feFlood floodColor={ink} result=\"inkC\" />\n      <feComposite in=\"inkC\" in2=\"core\" operator=\"in\" result=\"inked\" />\n      <feGaussianBlur data-k=\"haloBlur\" in=\"disp\" stdDeviation={0} result=\"haloSoft\" />\n      <feComposite\n        data-k=\"haloFib\"\n        in=\"haloSoft\"\n        in2=\"fibre\"\n        operator=\"arithmetic\"\n        k1={0}\n        k2={1}\n        k3={0}\n        k4={0}\n        result=\"haloFibrous\"\n      />\n      <feComponentTransfer in=\"haloFibrous\" result=\"haloA\">\n        <feFuncA data-k=\"haloA\" type=\"linear\" slope={0} intercept={0} />\n      </feComponentTransfer>\n      <feFlood floodColor={halo} result=\"haloC\" />\n      <feComposite in=\"haloC\" in2=\"haloA\" operator=\"in\" result=\"haloed\" />\n      <feMerge>\n        <feMergeNode in=\"haloed\" />\n        <feMergeNode in=\"inked\" />\n      </feMerge>\n    </filter>\n  );\n\n  return (\n    <div\n      ref={rootRef}\n      className={cn(\"relative flex w-full justify-center\", className)}\n      role={ariaLabel ? \"group\" : undefined}\n      aria-label={ariaLabel}\n    >\n      <span className=\"sr-only\">{text}</span>\n      <svg\n        aria-hidden=\"true\"\n        focusable=\"false\"\n        viewBox={vb ? vb.map((v) => v.toFixed(2)).join(\" \") : \"0 -100 600 130\"}\n        className=\"block h-auto w-full overflow-visible\"\n        style={{ maxWidth, aspectRatio: vb ? `${vb[2]} / ${vb[3]}` : undefined }}\n      >\n        <text\n          ref={measureRef}\n          x={0}\n          y={0}\n          fontSize={FS}\n          visibility=\"hidden\"\n          style={{ fontFamily, fontWeight, letterSpacing: `${letterSpacing}em`, fontKerning: \"normal\" }}\n        >\n          {text}\n        </text>\n        <defs>\n          <text\n            ref={textRef}\n            id={ids.text}\n            x={0}\n            y={0}\n            fontSize={FS}\n            fill={ink}\n            style={{ fontFamily, fontWeight, letterSpacing: `${letterSpacing}em`, fontKerning: \"normal\" }}\n          >\n            {text}\n          </text>\n          {vb ? (\n            <>\n              {renderFilter(ids.bleed, 0)}\n              {renderFilter(ids.wet, 11)}\n              {/* Fibrous edge for the wet fronts: displace a soft disc, then tighten its falloff. */}\n              <filter id={ids.fibre} filterUnits=\"userSpaceOnUse\" {...region} colorInterpolationFilters=\"sRGB\">\n                <feTurbulence type=\"fractalNoise\" baseFrequency=\"0.045 0.11\" numOctaves={3} seed={baseSeed + 5} result=\"n\" />\n                <feDisplacementMap in=\"SourceGraphic\" in2=\"n\" scale={FS * 0.22} xChannelSelector=\"R\" yChannelSelector=\"G\" result=\"d\" />\n                <feComponentTransfer in=\"d\">\n                  <feFuncA type=\"linear\" slope={2.6} intercept={-0.45} />\n                </feComponentTransfer>\n              </filter>\n              <radialGradient id={ids.drop}>\n                <stop offset=\"0\" stopColor=\"#fff\" />\n                <stop offset=\"0.62\" stopColor=\"#fff\" stopOpacity={0.9} />\n                <stop offset=\"1\" stopColor=\"#fff\" stopOpacity={0} />\n              </radialGradient>\n              <radialGradient id={ids.dropOut}>\n                <stop offset=\"0\" stopColor=\"#000\" />\n                <stop offset=\"0.5\" stopColor=\"#000\" stopOpacity={0.9} />\n                <stop offset=\"1\" stopColor=\"#000\" stopOpacity={0} />\n              </radialGradient>\n              <mask id={ids.reveal} maskUnits=\"userSpaceOnUse\" {...region}>\n                <g filter={`url(#${ids.fibre})`}>\n                  {dropSpecs.map((d, i) => (\n                    <circle key={i} ref={(el) => void (dropEls.current[i] = el)} cx={d.x} cy={d.y} r={0.01} fill={`url(#${ids.drop})`} />\n                  ))}\n                </g>\n              </mask>\n              <mask id={ids.wetIn} maskUnits=\"userSpaceOnUse\" {...region}>\n                <g filter={`url(#${ids.fibre})`}>\n                  <circle ref={(el) => void (wetCircles.current[0] = el)} r={0.01} fill={`url(#${ids.drop})`} />\n                </g>\n              </mask>\n              <mask id={ids.wetOut} maskUnits=\"userSpaceOnUse\" {...region}>\n                <rect {...region} fill=\"#fff\" />\n                <g filter={`url(#${ids.fibre})`}>\n                  <circle ref={(el) => void (wetCircles.current[1] = el)} r={0.01} fill={`url(#${ids.dropOut})`} />\n                </g>\n              </mask>\n            </>\n          ) : null}\n        </defs>\n        {vb ? (\n          <g ref={revealGroup}>\n            <g ref={bleedGroup}>\n              <g ref={crispGroup}>\n                <use href={`#${ids.text}`} />\n              </g>\n              <g ref={wetGroup} display=\"none\" mask={`url(#${ids.wetIn})`}>\n                <use href={`#${ids.text}`} filter={`url(#${ids.wet})`} />\n              </g>\n            </g>\n          </g>\n        ) : null}\n      </svg>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/text/ink-bleed-text.tsx"
    },
    {
      "path": "lib/utils.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n",
      "type": "registry:lib",
      "target": "@lib/utils.ts"
    },
    {
      "path": "components/bjork-ui/_core/palette.ts",
      "content": "export type BjorkTone = \"dark\" | \"light\";\n\n// Canvas, WebGL and SVG attribute colours. DOM code uses the CSS tokens with these as fallbacks.\nexport const BJORK_PALETTE = {\n  dark: {\n    bg: \"#050505\", stage: \"#111111\", surface: \"#121212\", raised: \"#161616\", active: \"#202020\",\n    border: \"#232323\", borderStrong: \"#343434\",\n    text: \"#ededed\", textMedium: \"rgba(237,237,237,0.72)\", textMuted: \"rgba(237,237,237,0.6)\",\n    textSoft: \"rgba(237,237,237,0.5)\", textFaint: \"rgba(237,237,237,0.22)\", hair: \"rgba(237,237,237,0.12)\",\n    accent: \"#ec5c13\", accentInk: \"#ec5c13\", accentFill: \"#b84a12\", accentSoft: \"rgba(236,92,19,0.12)\", accentMuted: \"rgba(236,92,19,0.24)\",\n    accentFg: \"#fff2ea\", success: \"#4cc38a\", warning: \"#f2b544\", error: \"#ff5c4d\",\n  },\n  light: {\n    bg: \"#f7f5ef\", stage: \"#f7f5ef\", surface: \"#fffcf6\", raised: \"#f5efe3\", active: \"#efe7d8\",\n    border: \"#eee6db\", borderStrong: \"#e1d7c8\",\n    text: \"#171717\", textMedium: \"rgba(23,23,23,0.78)\", textMuted: \"rgba(23,23,23,0.68)\",\n    textSoft: \"rgba(23,23,23,0.62)\", textFaint: \"rgba(23,23,23,0.22)\", hair: \"rgba(23,23,23,0.10)\",\n    accent: \"#ec7d43\", accentInk: \"#b4531f\", accentFill: \"#ec7d43\", accentSoft: \"rgba(236,125,67,0.10)\", accentMuted: \"rgba(236,125,67,0.20)\",\n    accentFg: \"#3f2112\", success: \"#1f8a55\", warning: \"#a86b00\", error: \"#c8361f\",\n  },\n} as const;\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/palette.ts"
    },
    {
      "path": "components/bjork-ui/_core/tone.ts",
      "content": "\"use client\";\n\nimport { useSyncExternalStore } from \"react\";\nimport { useTheme } from \"next-themes\";\nimport type { BjorkTone } from \"./palette\";\n\nconst subscribeNoop = () => () => {};\nconst getMounted = () => true;\nconst getServerMounted = () => false;\n\n/**\n * Resolves the tone for a component. An explicit `tone` always wins.\n * Otherwise it follows next-themes' resolved theme, but only after mount,\n * so the server render and first client render agree on dark.\n */\nexport function useBjorkTone(tone?: BjorkTone): BjorkTone {\n  const { resolvedTheme } = useTheme();\n  const mounted = useSyncExternalStore(subscribeNoop, getMounted, getServerMounted);\n  if (tone) return tone;\n  if (!mounted) return \"dark\";\n  return resolvedTheme === \"light\" ? \"light\" : \"dark\";\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/tone.ts"
    },
    {
      "path": "components/bjork-ui/_core/loop.ts",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, type RefObject } from \"react\";\n\nexport interface VisibleLoopOptions {\n  enabled?: boolean;\n  fpsCap?: number;\n  rootMargin?: string;\n}\n\n/**\n * Runs `frame(dt, t)` on requestAnimationFrame while the element is near the viewport,\n * the tab is visible and `enabled` is not false. Return `false` from `frame` to go idle\n * until `wake()` is called. Writes `data-loop` on the element: running, idle or paused.\n */\nexport function useVisibleLoop(\n  ref: RefObject<HTMLElement | null>,\n  frame: (dt: number, t: number) => boolean | void,\n  opts?: VisibleLoopOptions,\n): { wake: () => void } {\n  const enabled = opts?.enabled !== false;\n  const fpsCap = opts?.fpsCap ?? 0;\n  const rootMargin = opts?.rootMargin ?? \"96px\";\n\n  const frameRef = useRef(frame);\n  useEffect(() => {\n    frameRef.current = frame;\n  }, [frame]);\n\n  const wakeRef = useRef<() => void>(() => {});\n  const wake = useCallback(() => wakeRef.current(), []);\n\n  useEffect(() => {\n    const el = ref.current;\n    if (!el || typeof window === \"undefined\") return;\n\n    let wants = true; // false once the frame callback returns false\n    let inView = false;\n    let raf = 0;\n    let last = 0;\n    let t = 0;\n    const minFrame = fpsCap > 0 ? 1000 / fpsCap : 0;\n\n    const canRun = () => enabled && inView && document.visibilityState === \"visible\";\n    const stop = () => {\n      if (raf) cancelAnimationFrame(raf);\n      raf = 0;\n    };\n\n    const update = () => {\n      if (!enabled) {\n        stop();\n        el.dataset.loop = \"paused\";\n        return;\n      }\n      if (!wants) {\n        stop();\n        el.dataset.loop = \"idle\";\n        return;\n      }\n      if (!canRun()) {\n        stop();\n        el.dataset.loop = \"paused\";\n        return;\n      }\n      el.dataset.loop = \"running\";\n      if (!raf) {\n        last = performance.now();\n        raf = requestAnimationFrame(tick);\n      }\n    };\n\n    const tick = (now: number) => {\n      raf = 0;\n      if (!canRun() || !wants) {\n        update();\n        return;\n      }\n      const elapsed = now - last;\n      if (minFrame && elapsed < minFrame - 0.5) {\n        raf = requestAnimationFrame(tick);\n        return;\n      }\n      last = now;\n      const dt = Math.min(0.05, Math.max(0, elapsed / 1000));\n      t += dt;\n      const keep = frameRef.current(dt, t);\n      if (keep === false) {\n        wants = false;\n        update();\n        return;\n      }\n      raf = requestAnimationFrame(tick);\n    };\n\n    const observer = new IntersectionObserver(\n      ([entry]) => {\n        inView = entry.isIntersecting;\n        update();\n      },\n      { rootMargin },\n    );\n    observer.observe(el);\n\n    const onVisibility = () => update();\n    document.addEventListener(\"visibilitychange\", onVisibility);\n\n    wakeRef.current = () => {\n      wants = true;\n      update();\n    };\n\n    update();\n\n    return () => {\n      stop();\n      observer.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVisibility);\n      wakeRef.current = () => {};\n    };\n  }, [ref, enabled, fpsCap, rootMargin]);\n\n  return useMemo(() => ({ wake }), [wake]);\n}\n\n// matchMedia based check. False on the server.\nexport function isCoarsePointer(): boolean {\n  if (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") return false;\n  return window.matchMedia(\"(pointer: coarse)\").matches;\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/loop.ts"
    },
    {
      "path": "components/bjork-ui/_core/motion.ts",
      "content": "// Shared springs, easings and durations. Components import these instead of inventing values.\n\nexport const springs = {\n  press: { type: \"spring\", stiffness: 500, damping: 30, mass: 0.6 },\n  snappy: { type: \"spring\", stiffness: 400, damping: 25, mass: 1 },\n  standard: { type: \"spring\", stiffness: 300, damping: 30, mass: 0.8 },\n  soft: { type: \"spring\", stiffness: 200, damping: 25, mass: 1.2 },\n  settle: { type: \"spring\", stiffness: 380, damping: 30, mass: 0.8 },\n  blurIn: { type: \"spring\", stiffness: 400, damping: 28, mass: 0.6 },\n} as const;\n\nexport const ease = {\n  out: [0.23, 1, 0.32, 1],\n  inOut: [0.77, 0, 0.175, 1],\n  drawer: [0.32, 0.72, 0, 1],\n} as const;\n\nexport const easeCss = {\n  out: \"cubic-bezier(0.23,1,0.32,1)\",\n  inOut: \"cubic-bezier(0.77,0,0.175,1)\",\n  drawer: \"cubic-bezier(0.32,0.72,0,1)\",\n} as const;\n\nexport const durations = { press: 140, tooltip: 160, menu: 200, panel: 280 } as const; // ms\n\n// Cubic bezier easing for canvas and rAF code. Returns progress y for input x in [0, 1].\nexport function cubicBezier(x1: number, y1: number, x2: number, y2: number): (t: number) => number {\n  const cx = 3 * x1;\n  const bx = 3 * (x2 - x1) - cx;\n  const ax = 1 - cx - bx;\n  const cy = 3 * y1;\n  const by = 3 * (y2 - y1) - cy;\n  const ay = 1 - cy - by;\n  const sampleX = (s: number) => ((ax * s + bx) * s + cx) * s;\n  const sampleY = (s: number) => ((ay * s + by) * s + cy) * s;\n  const sampleDX = (s: number) => (3 * ax * s + 2 * bx) * s + cx;\n\n  const solveX = (x: number) => {\n    let s = x;\n    for (let i = 0; i < 8; i++) {\n      const err = sampleX(s) - x;\n      if (Math.abs(err) < 1e-6) return s;\n      const d = sampleDX(s);\n      if (Math.abs(d) < 1e-6) break;\n      s -= err / d;\n    }\n    let lo = 0;\n    let hi = 1;\n    s = x;\n    while (lo < hi) {\n      const v = sampleX(s);\n      if (Math.abs(v - x) < 1e-6) return s;\n      if (x > v) lo = s;\n      else hi = s;\n      s = (lo + hi) / 2;\n      if (hi - lo < 1e-7) break;\n    }\n    return s;\n  };\n\n  return (t: number) => {\n    if (t <= 0) return 0;\n    if (t >= 1) return 1;\n    return sampleY(solveX(t));\n  };\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/motion.ts"
    },
    {
      "path": "components/bjork-ui/_core/random.ts",
      "content": "// Seeded randomness and noise. Deterministic for a given seed, so previews and tests repeat.\n\n// Small fast PRNG. Returns floats in [0, 1).\nexport function mulberry32(seed: number): () => number {\n  let a = seed >>> 0;\n  return () => {\n    a = (a + 0x6d2b79f5) >>> 0;\n    let t = a;\n    t = Math.imul(t ^ (t >>> 15), t | 1);\n    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\n// FNV-1a 32-bit.\nexport function hashString(s: string): number {\n  let h = 0x811c9dc5;\n  for (let i = 0; i < s.length; i++) {\n    h ^= s.charCodeAt(i);\n    h = Math.imul(h, 0x01000193);\n  }\n  return h >>> 0;\n}\n\n// Integer lattice hash to [-1, 1].\nfunction latticeHash(seed: number, x: number, y = 0): number {\n  let h = Math.imul(x | 0, 374761393) ^ Math.imul(y | 0, 668265263) ^ Math.imul(seed | 0, 2246822519);\n  h = Math.imul(h ^ (h >>> 13), 1274126177);\n  h ^= h >>> 16;\n  return ((h >>> 0) / 4294967295) * 2 - 1;\n}\n\nfunction smooth(t: number): number {\n  return t * t * (3 - 2 * t);\n}\n\nexport function valueNoise1D(seed: number): (x: number) => number {\n  return (x: number) => {\n    const i = Math.floor(x);\n    const f = x - i;\n    const a = latticeHash(seed, i);\n    const b = latticeHash(seed, i + 1);\n    return a + (b - a) * smooth(f);\n  };\n}\n\nexport function fbm1D(seed: number, octaves = 3): (x: number) => number {\n  const layers = Array.from({ length: octaves }, (_, o) => valueNoise1D(seed + o * 101));\n  let norm = 0;\n  for (let o = 0; o < octaves; o++) norm += 0.5 ** o;\n  return (x: number) => {\n    let sum = 0;\n    let amp = 1;\n    let freq = 1;\n    for (let o = 0; o < octaves; o++) {\n      sum += amp * layers[o](x * freq);\n      amp *= 0.5;\n      freq *= 2;\n    }\n    return sum / norm;\n  };\n}\n\nexport function valueNoise2D(seed: number): (x: number, y: number) => number {\n  return (x: number, y: number) => {\n    const xi = Math.floor(x);\n    const yi = Math.floor(y);\n    const fx = smooth(x - xi);\n    const fy = smooth(y - yi);\n    const a = latticeHash(seed, xi, yi);\n    const b = latticeHash(seed, xi + 1, yi);\n    const c = latticeHash(seed, xi, yi + 1);\n    const d = latticeHash(seed, xi + 1, yi + 1);\n    const top = a + (b - a) * fx;\n    const bottom = c + (d - c) * fx;\n    return top + (bottom - top) * fy;\n  };\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/random.ts"
    }
  ],
  "cssVars": {
    "light": {
      "bjork-bg": "#f7f3ea",
      "bjork-surface": "#fffcf6",
      "bjork-surface-muted": "rgba(250, 246, 237, 0.94)",
      "bjork-surface-hover": "#f5efe3",
      "bjork-surface-active": "#efe7d8",
      "bjork-panel": "#f8f2e7",
      "bjork-menu": "rgba(255, 252, 246, 0.98)",
      "bjork-field": "rgba(255, 252, 246, 0.96)",
      "bjork-field-muted": "rgba(248, 242, 231, 0.82)",
      "bjork-field-inset": "rgba(239, 231, 216, 0.72)",
      "bjork-error-bg": "#fff0e8",
      "bjork-text": "#171717",
      "bjork-text-strong": "rgba(23, 23, 23, 0.9)",
      "bjork-text-medium": "rgba(23, 23, 23, 0.78)",
      "bjork-text-muted": "rgba(23, 23, 23, 0.68)",
      "bjork-text-soft": "rgba(23, 23, 23, 0.62)",
      "bjork-text-faint": "rgba(23, 23, 23, 0.22)",
      "bjork-inverted-text": "#fff8f0",
      "bjork-border": "#eee6db",
      "bjork-border-muted": "#f5ede2",
      "bjork-border-strong": "#e1d7c8",
      "bjork-thumb": "#a49b8e",
      "bjork-track": "#ded6ca",
      "bjork-accent": "#ec7d43",
      "bjork-accent-hover": "#f0935f",
      "bjork-accent-soft": "rgba(236, 125, 67, 0.1)",
      "bjork-accent-muted": "rgba(236, 125, 67, 0.2)",
      "bjork-accent-badge": "rgba(236, 125, 67, 0.16)",
      "bjork-accent-badge-foreground": "#8d421d",
      "bjork-accent-foreground": "#3f2112",
      "bjork-accent-fill": "#ec7d43",
      "bjork-accent-fill-hover": "#f0935f",
      "bjork-shadow-surface": "inset 0 7px 14px rgba(88, 72, 49, 0.045), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.92), inset 1px 0 0 rgba(88, 72, 49, 0.026), inset -1px 0 0 rgba(255, 255, 255, 0.68), 0 14px 22px -9px rgba(66, 52, 33, 0.11)",
      "bjork-shadow-soft": "inset 0 1px 0 rgba(88, 72, 49, 0.045), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.86)",
      "bjork-shadow-panel": "inset 0 1px 0 rgba(88, 72, 49, 0.04), inset 1px 0 0 rgba(88, 72, 49, 0.024), inset -1px 0 0 rgba(255, 255, 255, 0.64), 0 18px 38px -30px rgba(66, 52, 33, 0.2)",
      "bjork-shadow-menu": "inset 0 1px 0 rgba(88, 72, 49, 0.04), inset 0 12px 24px rgba(88, 72, 49, 0.022), inset 1px 0 0 rgba(88, 72, 49, 0.024), inset -1px 0 0 rgba(255, 255, 255, 0.62), 0 22px 44px -26px rgba(66, 52, 33, 0.22)",
      "bjork-shadow-inset": "inset 0 1px 10px rgba(88, 72, 49, 0.12), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.72)",
      "bjork-ring-offset": "#f7f3ea"
    },
    "dark": {
      "bjork-bg": "#111111",
      "bjork-surface": "#121212",
      "bjork-surface-muted": "rgba(22, 22, 22, 0.92)",
      "bjork-surface-hover": "#161616",
      "bjork-surface-active": "#202020",
      "bjork-panel": "#0d0d0d",
      "bjork-menu": "rgba(18, 18, 18, 0.98)",
      "bjork-field": "rgba(18, 18, 18, 0.95)",
      "bjork-field-muted": "rgba(18, 18, 18, 0.55)",
      "bjork-field-inset": "rgba(9, 9, 9, 0.75)",
      "bjork-error-bg": "#130d0a",
      "bjork-text": "#ededed",
      "bjork-text-strong": "rgba(237, 237, 237, 0.9)",
      "bjork-text-medium": "rgba(237, 237, 237, 0.72)",
      "bjork-text-muted": "rgba(237, 237, 237, 0.6)",
      "bjork-text-soft": "rgba(237, 237, 237, 0.5)",
      "bjork-text-faint": "rgba(237, 237, 237, 0.22)",
      "bjork-inverted-text": "#080808",
      "bjork-border": "#232323",
      "bjork-border-muted": "#1c1c1c",
      "bjork-border-strong": "#343434",
      "bjork-thumb": "#626262",
      "bjork-track": "#090909",
      "bjork-accent": "#ec5c13",
      "bjork-accent-hover": "#f07832",
      "bjork-accent-soft": "rgba(236, 92, 19, 0.12)",
      "bjork-accent-muted": "rgba(236, 92, 19, 0.24)",
      "bjork-accent-badge": "#b84a12",
      "bjork-accent-badge-foreground": "#fff2ea",
      "bjork-accent-foreground": "#fff2ea",
      "bjork-accent-fill": "#b84a12",
      "bjork-accent-fill-hover": "#a84a14",
      "bjork-shadow-surface": "inset 0 7px 14px rgba(255, 255, 255, 0.03), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.06), 0 14px 20px -6px rgba(0, 0, 0, 0.45)",
      "bjork-shadow-soft": "inset 0 1px 0 rgba(255, 255, 255, 0.045)",
      "bjork-shadow-panel": "inset 0 1px 0 rgba(255, 255, 255, 0.035), 0 18px 36px -28px rgba(0, 0, 0, 0.9)",
      "bjork-shadow-menu": "inset 0 1px 0 rgba(255, 255, 255, 0.055), inset 0 12px 24px rgba(255, 255, 255, 0.018), inset 0 -18px 26px rgba(0, 0, 0, 0.24), 0 22px 42px -22px rgba(0, 0, 0, 0.9)",
      "bjork-shadow-inset": "inset 0 1px 10px rgba(0, 0, 0, 0.45)",
      "bjork-ring-offset": "#080808"
    }
  },
  "css": {
    ".bjork-range": {
      "box-shadow": "inset 0 1px 1px rgba(255, 255, 255, 0.04), 0 0 14px rgba(189, 69, 20, 0.06)"
    },
    ".bjork-range::-webkit-slider-thumb": {
      "appearance": "none",
      "width": "16px",
      "height": "8px",
      "border": "0",
      "border-radius": "999px",
      "background": "var(--bjork-thumb)",
      "box-shadow": "inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 0 0 1px rgba(0, 0, 0, 0.42)"
    },
    ".bjork-range::-moz-range-thumb": {
      "width": "16px",
      "height": "8px",
      "border": "0",
      "border-radius": "999px",
      "background": "var(--bjork-thumb)",
      "box-shadow": "inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 0 0 1px rgba(0, 0, 0, 0.42)"
    },
    ".bjork-layered-button-accent, .bjork-layered-button-raised": {
      "position": "relative",
      "isolation": "isolate",
      "appearance": "none",
      "overflow": "hidden",
      "border": "0",
      "border-radius": "13px",
      "box-shadow": "inset 0 1px 0 #ffffff38, 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a",
      "transition": "filter 150ms ease-out, box-shadow 150ms ease-out"
    },
    ".bjork-layered-button-accent::after, .bjork-layered-button-raised::after": {
      "position": "absolute",
      "inset": "0",
      "pointer-events": "none",
      "content": "\"\"",
      "border": "1.5px solid transparent",
      "border-radius": "inherit",
      "background": "linear-gradient(#ffffffb8, #0000003d 41% 75%, #ffffff47) border-box",
      "mix-blend-mode": "overlay",
      "-webkit-mask-image": "linear-gradient(#000, #000), linear-gradient(#000, #000)",
      "mask-image": "linear-gradient(#000, #000), linear-gradient(#000, #000)",
      "-webkit-mask-clip": "padding-box, border-box",
      "mask-clip": "padding-box, border-box",
      "-webkit-mask-origin": "padding-box, border-box",
      "mask-origin": "padding-box, border-box",
      "-webkit-mask-composite": "xor",
      "mask-composite": "exclude"
    },
    ".bjork-layered-button-accent": {
      "color": "oklch(100% 0 0)",
      "text-shadow": "0 -1px #00000040",
      "background": "radial-gradient(ellipse at -20px top, rgba(255, 255, 255, 0.14), transparent), linear-gradient(180deg, #ec5c13 0%, #ec5c13 62%, color-mix(in oklab, #ec5c13 84%, black 16%) 100%)"
    },
    ".bjork-layered-button-raised": {
      "color": "oklch(100% 0 0)",
      "text-shadow": "0 -1px #00000026",
      "background": "radial-gradient(ellipse at -20px top, #ffffff38, #fff0), linear-gradient(180deg, oklch(0.1881 0.006 265), oklch(0.5493 0.0081 265))"
    },
    ".dark .bjork-layered-button-raised": {
      "color": "oklch(0.2574 0.0056 265)",
      "text-shadow": "0 -1px #00000026",
      "background": "radial-gradient(ellipse at -20px top, #ffffff38, #fff0), linear-gradient(180deg, color-mix(in oklab, oklch(0.9674 0.0013 265) 80%, oklch(0.2574 0.0056 265) 20%), color-mix(in oklab, oklch(0.669 0.0107 265) 80%, oklch(0.2574 0.0056 265) 20%))"
    },
    "@media (hover: hover)": {
      ".bjork-layered-button-accent:hover": {
        "filter": "brightness(1.08) saturate(1.04)"
      },
      ".bjork-layered-button-raised:hover": {
        "filter": "brightness(1.08) contrast(0.9)"
      }
    }
  },
  "meta": {
    "collection": "Text",
    "registryUrl": "https://ui.isaiahbjork.com/ink-bleed-text.json",
    "installCommand": "npx shadcn@latest add https://ui.isaiahbjork.com/ink-bleed-text.json"
  },
  "type": "registry:component"
}