{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "GooeyNav-TS-TW",
	"title": "GooeyNav",
	"description": "A navigation bar whose active pill breaks into goo that flows over to the item you pick.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "GooeyNav/GooeyNav.tsx",
			"content": "'use client';\n\nimport {\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  useState,\n  type CSSProperties,\n  type MouseEvent,\n  type ReactNode\n} from 'react';\n\nconst useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;\n\ntype Palette = 'dark' | 'light';\ntype Size = 'sm' | 'md' | 'lg';\ntype Rgba = number[];\n\nexport interface GooeyNavItem {\n  label?: string;\n  href?: string;\n  icon?: ReactNode;\n}\n\ninterface GooeyNavProps {\n  items?: GooeyNavItem[];\n  initialActiveIndex?: number;\n  activeIndex?: number;\n  onChange?: (index: number, item: GooeyNavItem) => void;\n  theme?: Palette;\n  color?: string;\n  activeTextColor?: string;\n  colors?: string[];\n  size?: Size;\n  frame?: boolean;\n  animationTime?: number;\n  particleCount?: number;\n  timeVariance?: number;\n  spread?: number;\n  gooeyness?: number;\n  wobble?: number;\n  hoverEffect?: boolean;\n  className?: string;\n  style?: CSSProperties;\n}\n\ninterface Rect {\n  x: number;\n  y: number;\n  w: number;\n  h: number;\n}\n\ninterface PillState {\n  mass: number;\n  scale: number;\n  velocity: number;\n  outgoing: number;\n}\n\ninterface Drop {\n  x: number;\n  y: number;\n  vx: number;\n  vy: number;\n  mass: number;\n  size: number;\n  given: number;\n  source: number;\n  index: number;\n  leave: number;\n  emerged: number;\n  absorbed: number;\n  state: 'waiting' | 'flying' | 'absorbing';\n  tint: Rgba | null;\n  start?: number;\n  duration?: number;\n  path?: number[];\n}\n\ninterface Box {\n  cx: number;\n  cy: number;\n  hw: number;\n  hh: number;\n}\n\ninterface Blob {\n  x: number;\n  y: number;\n  r: number;\n  tx: number;\n  ty: number;\n  tint: Rgba | null;\n}\n\ninterface Pipeline {\n  program: WebGLProgram;\n  uniforms: Record<string, WebGLUniformLocation | null>;\n  buffer: WebGLBuffer;\n  position: number;\n}\n\nconst THEMES: Record<Palette, Record<string, string>> = {\n  dark: {\n    '--gn-frame': 'rgba(38, 35, 46, 0.66)',\n    '--gn-frame-edge': 'rgba(255, 255, 255, 0.08)',\n    '--gn-frame-highlight': 'rgba(255, 255, 255, 0.07)',\n    '--gn-frame-shadow': '0 22px 44px -20px rgba(0, 0, 0, 0.8), 0 6px 16px -8px rgba(0, 0, 0, 0.5)',\n    '--gn-ink': '#f4f4f5',\n    '--gn-muted': 'rgba(244, 244, 245, 0.62)'\n  },\n  light: {\n    '--gn-frame': 'rgba(240, 240, 243, 0.8)',\n    '--gn-frame-edge': 'rgba(24, 24, 27, 0.07)',\n    '--gn-frame-highlight': 'rgba(255, 255, 255, 0.95)',\n    '--gn-frame-shadow': '0 22px 44px -22px rgba(24, 24, 27, 0.3), 0 6px 16px -10px rgba(24, 24, 27, 0.16)',\n    '--gn-ink': '#27272a',\n    '--gn-muted': 'rgba(39, 39, 42, 0.62)'\n  }\n};\n\nconst PILLS: Record<Palette, { color: string; hover: string; shadow: number }> = {\n  dark: { color: '#f4f4f5', hover: 'rgba(255, 255, 255, 0.08)', shadow: 0.4 },\n  light: { color: '#27272a', hover: 'rgba(24, 24, 27, 0.06)', shadow: 0.2 }\n};\n\nconst SIZES: Record<Size, Record<string, string>> = {\n  sm: { '--gn-pad': '5px', '--gn-gap': '2px', '--gn-font': '13px', '--gn-x': '13px', '--gn-y': '7px' },\n  md: { '--gn-pad': '6px', '--gn-gap': '4px', '--gn-font': '14px', '--gn-x': '16px', '--gn-y': '9px' },\n  lg: { '--gn-pad': '7px', '--gn-gap': '6px', '--gn-font': '16px', '--gn-x': '20px', '--gn-y': '11px' }\n};\n\nconst ROOT = 'relative inline-flex max-w-full text-[length:var(--gn-font)] leading-[1.2]';\nconst FRAMED =\n  'rounded-full p-[var(--gn-pad)] backdrop-blur-[20px] backdrop-saturate-[1.6] [background:var(--gn-frame)] [box-shadow:var(--gn-frame-shadow)]';\nconst LIST_BASE = 'm-0 flex list-none flex-wrap gap-[var(--gn-gap)] p-0';\nconst LIST = `relative ${LIST_BASE}`;\nconst OVERLAY = `${LIST_BASE} pointer-events-none invisible absolute inset-0 text-[var(--gn-active-ink)] [clip-path:inset(100%)]`;\nconst LINK_BASE =\n  'inline-flex items-center gap-2 whitespace-nowrap rounded-full border-none bg-transparent px-[var(--gn-x)] py-[var(--gn-y)] [font-family:inherit] [font-size:inherit] [line-height:inherit] font-medium no-underline';\nconst LINK = `${LINK_BASE} cursor-pointer text-[var(--gn-muted)] transition-colors duration-200 [-webkit-tap-highlight-color:transparent] group-data-[active]:text-[var(--gn-ink)] hover:text-[var(--gn-ink)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--gn-ink)]`;\nconst GHOST = `${LINK_BASE} text-inherit`;\nconst ICON = 'inline-flex flex-none [&_svg]:h-[1.15em] [&_svg]:w-[1.15em]';\nconst RIM =\n  'pointer-events-none absolute inset-0 rounded-[inherit] [box-shadow:inset_0_0_0_1px_var(--gn-frame-edge),inset_0_1px_0_var(--gn-frame-highlight)]';\n\nconst DEFAULT_ITEMS: GooeyNavItem[] = [\n  { label: 'Home', href: '#' },\n  { label: 'Work', href: '#' },\n  { label: 'About', href: '#' },\n  { label: 'Contact', href: '#' }\n];\n\nconst MAX_DROPS = 32;\nconst MAX_PILLS = 4;\nconst MARGIN = 72;\n\nconst VERTEX = `#version 300 es\nin vec2 aPosition;\nvoid main() {\n  gl_Position = vec4(aPosition, 0.0, 1.0);\n}\n`;\n\nconst FRAGMENT = `#version 300 es\nprecision highp float;\n\nuniform vec2 uSize;\nuniform float uRatio;\nuniform vec4 uPills[${MAX_PILLS}];\nuniform vec4 uHover;\nuniform vec4 uDrops[${MAX_DROPS}];\nuniform vec2 uTails[${MAX_DROPS}];\nuniform vec3 uTints[${MAX_DROPS}];\nuniform int uCount;\nuniform float uSmooth;\nuniform vec3 uPill;\nuniform vec4 uHoverColor;\nuniform float uShadow;\n\nout vec4 outColor;\n\nfloat sdPill(vec2 p, vec4 box) {\n  vec2 extent = max(box.zw, vec2(0.0));\n  float r = min(extent.x, extent.y);\n  vec2 q = abs(p - box.xy) - extent + r;\n  return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;\n}\n\nfloat sdCapsule(vec2 p, vec2 a, vec2 b, float r) {\n  vec2 pa = p - a;\n  vec2 ba = b - a;\n  float h = clamp(dot(pa, ba) / max(dot(ba, ba), 0.0001), 0.0, 1.0);\n  return length(pa - ba * h) - r;\n}\n\nvec4 blend(vec4 a, vec4 b, float k) {\n  float h = clamp(0.5 + 0.5 * (b.w - a.w) / k, 0.0, 1.0);\n  return vec4(mix(b.rgb, a.rgb, h), mix(b.w, a.w, h) - k * h * (1.0 - h));\n}\n\nvec4 field(vec2 p) {\n  vec4 shape = vec4(uPill, 1e5);\n  for (int i = 0; i < ${MAX_PILLS}; i++) {\n    vec4 pill = uPills[i];\n    if (pill.z <= 0.0) continue;\n    shape = blend(shape, vec4(uPill, sdPill(p, pill)), uSmooth);\n  }\n  for (int i = 0; i < ${MAX_DROPS}; i++) {\n    if (i >= uCount) break;\n    vec4 drop = uDrops[i];\n    if (drop.z <= 0.0) continue;\n    float d = sdCapsule(p, drop.xy, drop.xy - uTails[i], drop.z);\n    if (d > shape.w + uSmooth) continue;\n    shape = blend(shape, vec4(uTints[i], d), uSmooth);\n  }\n  return shape;\n}\n\nvoid main() {\n  vec2 p = vec2(gl_FragCoord.x, uSize.y * uRatio - gl_FragCoord.y) / uRatio;\n  vec4 shape = field(p);\n  float d = shape.w;\n  float aa = max(fwidth(d), 0.35);\n  float body = clamp(0.5 - d / aa, 0.0, 1.0);\n\n  vec2 grad = vec2(dFdx(d), -dFdy(d));\n  vec2 n = length(grad) > 0.0001 ? normalize(grad) : vec2(0.0, 1.0);\n  float rim = (1.0 - smoothstep(0.0, 1.6, -d)) * max(0.0, -n.y) * 0.3;\n  vec3 fill = mix(shape.rgb, vec3(1.0), rim) * (1.0 - 0.06 * max(0.0, n.y) * (1.0 - smoothstep(0.0, 6.0, -d)));\n\n  float lifted = field(p - vec2(0.0, 3.0)).w;\n  float shadow = (1.0 - smoothstep(-3.0, 9.0, lifted)) * uShadow * (1.0 - body);\n\n  float hover = clamp(0.5 - sdPill(p, uHover) / aa, 0.0, 1.0) * uHoverColor.a;\n\n  vec4 color = vec4(0.0, 0.0, 0.0, shadow);\n  color = vec4(uHoverColor.rgb * hover, hover) + color * (1.0 - hover);\n  color = vec4(fill * body, body) + color * (1.0 - body);\n  outColor = color;\n}\n`;\n\nconst parse = (value: string): Rgba => {\n  const match = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(String(value).trim());\n  if (match) {\n    const hex = match[1].length === 3 ? match[1].replace(/./g, digit => digit + digit) : match[1];\n    return [0, 2, 4].map(index => parseInt(hex.slice(index, index + 2), 16) / 255).concat(1);\n  }\n  const rgba = /rgba?\\(([^)]+)\\)/i.exec(String(value));\n  if (rgba) {\n    const parts = rgba[1].split(',').map(part => parseFloat(part));\n    return [parts[0] / 255, parts[1] / 255, parts[2] / 255, parts[3] ?? 1];\n  }\n  return [1, 1, 1, 1];\n};\n\nconst contrastInk = (rgb: Rgba) => (0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2] > 0.55 ? '#18161d' : '#ffffff');\n\nconst clamp01 = (value: number) => Math.min(1, Math.max(0, value));\n\nconst travelEase = (t: number) => {\n  const x = clamp01(t);\n  return x < 0.5 ? 4 * x * x * x : 1 - (-2 * x + 2) ** 3 / 2;\n};\n\nconst bezier = (a: number, b: number, c: number, d: number, t: number) => {\n  const u = 1 - t;\n  return u * u * u * a + 3 * u * u * t * b + 3 * u * t * t * c + t * t * t * d;\n};\n\nconst pillPath = (x: number, y: number, w: number, h: number) => {\n  if (w <= 0.5 || h <= 0.5) return '';\n  const r = Math.min(w, h) / 2;\n  return `M${x + r},${y}H${x + w - r}A${r},${r} 0 0 1 ${x + w},${y + r}V${y + h - r}A${r},${r} 0 0 1 ${x + w - r},${y + h}H${x + r}A${r},${r} 0 0 1 ${x},${y + h - r}V${y + r}A${r},${r} 0 0 1 ${x + r},${y}Z`;\n};\n\nconst compile = (gl: WebGL2RenderingContext, type: number, source: string) => {\n  const shader = gl.createShader(type) as WebGLShader;\n  gl.shaderSource(shader, source);\n  gl.compileShader(shader);\n  return shader;\n};\n\nconst UNIFORMS = [\n  'uSize',\n  'uRatio',\n  'uPills',\n  'uHover',\n  'uDrops',\n  'uTails',\n  'uTints',\n  'uCount',\n  'uSmooth',\n  'uPill',\n  'uHoverColor',\n  'uShadow'\n];\n\nconst build = (gl: WebGL2RenderingContext): Pipeline | null => {\n  const program = gl.createProgram() as WebGLProgram;\n  gl.attachShader(program, compile(gl, gl.VERTEX_SHADER, VERTEX));\n  gl.attachShader(program, compile(gl, gl.FRAGMENT_SHADER, FRAGMENT));\n  gl.linkProgram(program);\n  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n    gl.deleteProgram(program);\n    return null;\n  }\n  const uniforms = Object.fromEntries(UNIFORMS.map(name => [name, gl.getUniformLocation(program, name)]));\n  const buffer = gl.createBuffer() as WebGLBuffer;\n  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW);\n  return { program, uniforms, buffer, position: gl.getAttribLocation(program, 'aPosition') };\n};\n\nconst GooeyNav = ({\n  items = DEFAULT_ITEMS,\n  initialActiveIndex = 0,\n  activeIndex,\n  onChange,\n  theme = 'dark',\n  color,\n  activeTextColor,\n  colors = [],\n  size = 'md',\n  frame = true,\n  animationTime = 600,\n  particleCount = 15,\n  timeVariance = 300,\n  spread = 56,\n  gooeyness = 0.5,\n  wobble = 0.5,\n  hoverEffect = true,\n  className = '',\n  style\n}: GooeyNavProps) => {\n  const [innerIndex, setInnerIndex] = useState(initialActiveIndex);\n  const current = Math.min(Math.max(0, activeIndex ?? innerIndex), Math.max(0, items.length - 1));\n  const stageRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const listRef = useRef<HTMLUListElement>(null);\n  const overlayRef = useRef<HTMLUListElement>(null);\n  const fallbackRef = useRef<HTMLSpanElement>(null);\n  const engineRef = useRef<{ go: (index: number) => void; refresh: () => void } | null>(null);\n  const indexRef = useRef(current);\n  indexRef.current = current;\n  const palette = theme === 'light' ? 'light' : 'dark';\n  const pillColor = color || PILLS[palette].color;\n  const pillRgb = parse(pillColor);\n  const settings = {\n    animationTime,\n    particleCount,\n    timeVariance,\n    spread,\n    gooeyness,\n    wobble,\n    hoverEffect,\n    pillRgb,\n    tints: colors.map(parse),\n    hoverRgb: parse(PILLS[palette].hover),\n    shadow: PILLS[palette].shadow\n  };\n  const settingsRef = useRef(settings);\n  settingsRef.current = settings;\n\n  const select = (index: number) => {\n    if (index === current) return;\n    if (activeIndex === undefined) setInnerIndex(index);\n    onChange?.(index, items[index]);\n  };\n\n  useIsomorphicLayoutEffect(() => {\n    const stage = stageRef.current;\n    const canvas = canvasRef.current;\n    const list = listRef.current;\n    const overlay = overlayRef.current;\n    const fallback = fallbackRef.current;\n    if (!stage || !canvas || !list || !overlay || !fallback) return;\n\n    const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n    const gl = canvas.getContext('webgl2', { alpha: true, premultipliedAlpha: true, antialias: false });\n    const pipeline = gl ? build(gl) : null;\n    fallback.style.display = pipeline ? 'none' : 'block';\n    canvas.style.display = pipeline ? 'block' : 'none';\n\n    const view = { width: 0, height: 0 };\n    let rects: Rect[] = [];\n    let target = -1;\n    const pills = new Map<number, PillState>();\n    const drops: Drop[] = [];\n    const hover = { x: 0, y: 0, w: 0, h: 0, alpha: 0, index: -1 };\n    const pillData = new Float32Array(MAX_PILLS * 4);\n    const dropData = new Float32Array(MAX_DROPS * 4);\n    const tailData = new Float32Array(MAX_DROPS * 2);\n    const tintData = new Float32Array(MAX_DROPS * 3);\n    let raf = 0;\n    let last = 0;\n    let disposed = false;\n\n    const measure = () => {\n      const box = stage.getBoundingClientRect();\n      const ratio = Math.min(window.devicePixelRatio || 1, 2);\n      view.width = box.width;\n      view.height = box.height;\n      const outerWidth = box.width + MARGIN * 2;\n      const outerHeight = box.height + MARGIN * 2;\n      Object.assign(canvas.style, {\n        left: `${-MARGIN}px`,\n        top: `${-MARGIN}px`,\n        width: `${outerWidth}px`,\n        height: `${outerHeight}px`\n      });\n      const width = Math.max(1, Math.round(outerWidth * ratio));\n      const height = Math.max(1, Math.round(outerHeight * ratio));\n      if (canvas.width !== width || canvas.height !== height) {\n        canvas.width = width;\n        canvas.height = height;\n      }\n      rects = Array.from(list.children).map(item => {\n        const rect = item.getBoundingClientRect();\n        return { x: rect.left - box.left, y: rect.top - box.top, w: rect.width, h: rect.height };\n      });\n    };\n\n    const trace = (boxes: Box[], blobs: Blob[], smooth: number) => {\n      const field = (x: number, y: number) => {\n        let d = 1e5;\n        const merge = (value: number) => {\n          const h = clamp01(0.5 + (0.5 * (value - d)) / smooth);\n          d = value + (d - value) * h - smooth * h * (1 - h);\n        };\n        for (const box of boxes) {\n          const r = Math.min(box.hw, box.hh);\n          const qx = Math.abs(x - box.cx) - box.hw + r;\n          const qy = Math.abs(y - box.cy) - box.hh + r;\n          merge(Math.hypot(Math.max(qx, 0), Math.max(qy, 0)) + Math.min(Math.max(qx, qy), 0) - r);\n        }\n        for (const blob of blobs) {\n          const px = x - blob.x;\n          const py = y - blob.y;\n          const bx = -blob.tx;\n          const by = -blob.ty;\n          const span = bx * bx + by * by;\n          const h = span > 0.0001 ? clamp01((px * bx + py * by) / span) : 0;\n          const value = Math.hypot(px - bx * h, py - by * h) - blob.r;\n          if (value > d + smooth) continue;\n          merge(value);\n        }\n        return d;\n      };\n      const step = 2;\n      const parts: string[] = [];\n      for (let y = 0; y < view.height; y += 1) {\n        const cy = y + 0.5;\n        let previous = field(0, cy);\n        let start: number | null = previous < 0 ? 0 : null;\n        for (let x = step; x <= view.width + step; x += step) {\n          const value = field(Math.min(x, view.width), cy);\n          if (previous < 0 !== value < 0) {\n            const cross = x - step + (step * previous) / (previous - value);\n            if (value < 0) start = cross;\n            else if (start !== null) {\n              parts.push(`M${start.toFixed(1)},${y}H${cross.toFixed(1)}V${y + 1}H${start.toFixed(1)}Z`);\n              start = null;\n            }\n          }\n          previous = value;\n        }\n        if (start !== null) parts.push(`M${start.toFixed(1)},${y}H${view.width}V${y + 1}H${start.toFixed(1)}Z`);\n      }\n      return parts.join('');\n    };\n\n    const pill = (index: number): PillState => {\n      if (!pills.has(index)) pills.set(index, { mass: 0, scale: 0, velocity: 0, outgoing: 0 });\n      return pills.get(index) as PillState;\n    };\n\n    const shape = (index: number, amount: number): Rect | null => {\n      const rect = rects[index];\n      if (!rect || amount <= 0.002) return null;\n      const h = rect.h * Math.min(1.08, 0.42 + 0.58 * Math.sqrt(Math.max(0, amount)));\n      const w = Math.max(h, rect.w * Math.max(0, amount) ** 0.75);\n      return { x: rect.x + rect.w / 2 - w / 2, y: rect.y + rect.h / 2 - h / 2, w, h };\n    };\n\n    const plan = (drop: Drop, index: number, now: number, fresh: boolean) => {\n      const s = settingsRef.current;\n      const rect = rects[index];\n      if (!rect) return;\n      const reach = Math.max(0, s.spread);\n      const ex = rect.x + rect.w / 2 + (Math.random() - 0.5) * rect.w * 0.45;\n      const ey = rect.y + rect.h / 2 + (Math.random() - 0.5) * rect.h * 0.2;\n      const base = Math.max(0.15, s.animationTime / 1000);\n      const variance = (Math.random() - 0.5) * (Math.max(0, s.timeVariance) / 1000);\n      const duration = Math.max(0.15, base * (0.62 + Math.random() * 0.32) + variance * 0.6);\n      const out = Math.random() * Math.PI * 2;\n      const into = Math.random() * Math.PI * 2;\n      const burst = reach * (0.7 + Math.random() * 0.6);\n      const swing = reach * (0.55 + Math.random() * 0.55);\n      const carry = Math.min(duration / 3, 0.12);\n      drop.index = index;\n      drop.start = now;\n      drop.duration = duration;\n      drop.path = [\n        drop.x,\n        drop.y,\n        fresh ? drop.x + Math.cos(out) * burst * 1.6 : drop.x + drop.vx * carry,\n        fresh ? drop.y + Math.sin(out) * burst * 1.6 : drop.y + drop.vy * carry,\n        ex + Math.cos(into) * swing * 1.5,\n        ey + Math.sin(into) * swing * 1.5,\n        ex,\n        ey\n      ];\n    };\n\n    const emit = (from: number, to: number, now: number) => {\n      const s = settingsRef.current;\n      const source = pills.get(from);\n      const rect = rects[from];\n      if (!source || !rect || !rects[to]) return;\n      const available = source.mass - source.outgoing;\n      if (available <= 0.002) return;\n      const room = MAX_DROPS - drops.length;\n      const count = Math.min(room, Math.max(1, Math.round(Math.max(1, s.particleCount) * available)));\n      if (count <= 0) return;\n      const window = Math.max(0.15, s.animationTime / 1000) * 0.16;\n      for (let i = 0; i < count; i++) {\n        const mass = available / count;\n        source.outgoing += mass;\n        drops.push({\n          x: rect.x + rect.w / 2 + (Math.random() - 0.5) * rect.w * 0.6,\n          y: rect.y + rect.h / 2 + (Math.random() - 0.5) * rect.h * 0.3,\n          vx: 0,\n          vy: 0,\n          mass,\n          size: 0.8 + Math.random() * 0.4,\n          given: 0,\n          source: from,\n          index: to,\n          leave: now + Math.random() * window * 1000,\n          emerged: 0,\n          absorbed: 0,\n          state: 'waiting',\n          tint: s.tints.length ? s.tints[Math.floor(Math.random() * s.tints.length)] : null\n        });\n      }\n    };\n\n    const render = () => {\n      const s = settingsRef.current;\n      const height = rects[target]?.h ?? rects[0]?.h ?? 36;\n      const smooth = height * (0.18 + 0.5 * clamp01(s.gooeyness));\n      const boxes: Box[] = [];\n      const blobs: Blob[] = [];\n      for (const [index, state] of pills) {\n        const box = shape(index, state.scale);\n        if (box) boxes.push({ cx: box.x + box.w / 2, cy: box.y + box.h / 2, hw: box.w / 2, hh: box.h / 2 });\n      }\n      for (const drop of drops) {\n        if (drop.state === 'waiting') continue;\n        const radius = 0.26 * height * drop.size * Math.min(drop.emerged, 1 - drop.absorbed);\n        if (radius <= 0.05) continue;\n        const speed = Math.hypot(drop.vx, drop.vy);\n        const length = Math.min(radius * 1.1, speed * 0.018);\n        blobs.push({\n          x: drop.x,\n          y: drop.y,\n          r: radius,\n          tx: speed > 0.01 ? (drop.vx / speed) * length : 0,\n          ty: speed > 0.01 ? (drop.vy / speed) * length : 0,\n          tint: drop.tint\n        });\n      }\n\n      const settledPill = boxes.length === 1 && !blobs.length;\n      if (settledPill) {\n        const box = boxes[0];\n        overlay.style.clipPath = `path('${pillPath(box.cx - box.hw, box.cy - box.hh, box.hw * 2, box.hh * 2)}')`;\n      } else {\n        overlay.style.clipPath = `path('${trace(boxes, blobs, smooth) || 'M0,0Z'}')`;\n      }\n      overlay.style.visibility = 'visible';\n\n      if (!pipeline || !gl) {\n        const box = shape(target, 1);\n        if (box) {\n          fallback.style.transform = `translate(${box.x.toFixed(2)}px, ${box.y.toFixed(2)}px)`;\n          fallback.style.width = `${box.w}px`;\n          fallback.style.height = `${box.h}px`;\n        }\n        fallback.style.opacity = box ? '1' : '0';\n        return;\n      }\n\n      let slot = 0;\n      for (const box of boxes) {\n        if (slot >= MAX_PILLS) break;\n        pillData[slot * 4] = box.cx + MARGIN;\n        pillData[slot * 4 + 1] = box.cy + MARGIN;\n        pillData[slot * 4 + 2] = box.hw;\n        pillData[slot * 4 + 3] = box.hh;\n        slot += 1;\n      }\n      pillData.fill(0, slot * 4);\n      let count = 0;\n      for (const blob of blobs) {\n        if (count >= MAX_DROPS) break;\n        dropData[count * 4] = blob.x + MARGIN;\n        dropData[count * 4 + 1] = blob.y + MARGIN;\n        dropData[count * 4 + 2] = blob.r;\n        dropData[count * 4 + 3] = 0;\n        tailData[count * 2] = blob.tx;\n        tailData[count * 2 + 1] = blob.ty;\n        const tint = blob.tint || s.pillRgb;\n        tintData[count * 3] = tint[0];\n        tintData[count * 3 + 1] = tint[1];\n        tintData[count * 3 + 2] = tint[2];\n        count += 1;\n      }\n\n      const { program, uniforms, buffer, position } = pipeline;\n      gl.viewport(0, 0, canvas.width, canvas.height);\n      gl.clearColor(0, 0, 0, 0);\n      gl.clear(gl.COLOR_BUFFER_BIT);\n      gl.useProgram(program);\n      gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n      gl.enableVertexAttribArray(position);\n      gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);\n      gl.uniform2f(uniforms.uSize, view.width + MARGIN * 2, view.height + MARGIN * 2);\n      gl.uniform1f(uniforms.uRatio, canvas.width / Math.max(1, view.width + MARGIN * 2));\n      gl.uniform4fv(uniforms.uPills, pillData);\n      gl.uniform4f(uniforms.uHover, hover.x + MARGIN, hover.y + MARGIN, hover.w / 2, hover.h / 2);\n      gl.uniform4fv(uniforms.uDrops, dropData);\n      gl.uniform2fv(uniforms.uTails, tailData);\n      gl.uniform3fv(uniforms.uTints, tintData);\n      gl.uniform1i(uniforms.uCount, count);\n      gl.uniform1f(uniforms.uSmooth, smooth);\n      gl.uniform3f(uniforms.uPill, s.pillRgb[0], s.pillRgb[1], s.pillRgb[2]);\n      gl.uniform4f(uniforms.uHoverColor, s.hoverRgb[0], s.hoverRgb[1], s.hoverRgb[2], s.hoverRgb[3] * hover.alpha);\n      gl.uniform1f(uniforms.uShadow, s.shadow);\n      gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);\n    };\n\n    const frame = (now: number) => {\n      raf = 0;\n      const s = settingsRef.current;\n      const dt = Math.min(0.05, (now - last) / 1000 || 0);\n      last = now;\n      const emergeTime = 0.09;\n      const absorbTime = 0.12;\n\n      for (let i = drops.length - 1; i >= 0; i--) {\n        const drop = drops[i];\n        if (drop.state === 'waiting') {\n          if (now < drop.leave) continue;\n          drop.state = 'flying';\n          plan(drop, drop.index, now, true);\n        }\n        if (drop.state === 'flying') {\n          drop.emerged = Math.min(1, drop.emerged + dt / emergeTime);\n          const source = pills.get(drop.source);\n          if (source && drop.given < drop.mass) {\n            const step = Math.min(drop.mass * (dt / emergeTime), drop.mass - drop.given);\n            source.mass = Math.max(0, source.mass - step);\n            source.outgoing = Math.max(0, source.outgoing - step);\n            drop.given += step;\n          }\n          const t = clamp01((now - (drop.start ?? now)) / 1000 / (drop.duration ?? 1));\n          const eased = travelEase(t);\n          const [x0, y0, x1, y1, x2, y2, x3, y3] = drop.path ?? [\n            drop.x,\n            drop.y,\n            drop.x,\n            drop.y,\n            drop.x,\n            drop.y,\n            drop.x,\n            drop.y\n          ];\n          const x = bezier(x0, x1, x2, x3, eased);\n          const y = bezier(y0, y1, y2, y3, eased);\n          if (dt > 0) {\n            drop.vx = (x - drop.x) / dt;\n            drop.vy = (y - drop.y) / dt;\n          }\n          drop.x = x;\n          drop.y = y;\n          if (t >= 1) drop.state = 'absorbing';\n        }\n        if (drop.state === 'absorbing') {\n          const step = Math.min(1 - drop.absorbed, dt / absorbTime);\n          drop.absorbed += step;\n          drop.vx *= 0.8;\n          drop.vy *= 0.8;\n          const host = pill(drop.index);\n          host.mass = Math.min(1, host.mass + drop.mass * step);\n          if (drop.absorbed >= 1) drops.splice(i, 1);\n        }\n      }\n\n      const zeta = reduced ? 1 : 1 - 0.72 * clamp01(s.wobble);\n      const omega = 26;\n      const steps = Math.max(1, Math.ceil(dt / (1 / 240)));\n      const h = dt / steps;\n      for (const [index, state] of pills) {\n        for (let n = 0; n < steps; n++) {\n          state.velocity += (omega * omega * (state.mass - state.scale) - 2 * zeta * omega * state.velocity) * h;\n          state.scale += state.velocity * h;\n        }\n        if (index !== target && state.mass <= 0.002 && state.outgoing <= 0.002 && Math.abs(state.scale) < 0.004) {\n          pills.delete(index);\n        }\n      }\n\n      if (hover.index >= 0 && rects[hover.index]) {\n        const rect = rects[hover.index];\n        const k = 1 - Math.exp(-dt / 0.06);\n        hover.x += (rect.x + rect.w / 2 - hover.x) * k;\n        hover.y += (rect.y + rect.h / 2 - hover.y) * k;\n        hover.w += (rect.w - hover.w) * k;\n        hover.h += (rect.h - hover.h) * k;\n      }\n      const hoverGoal = s.hoverEffect && hover.index >= 0 && hover.index !== target ? 1 : 0;\n      hover.alpha += (hoverGoal - hover.alpha) * (1 - Math.exp(-dt / 0.08));\n\n      render();\n\n      const host = pills.get(target);\n      const resting =\n        drops.length === 0 &&\n        pills.size === 1 &&\n        host &&\n        Math.abs(host.mass - host.scale) < 0.0008 &&\n        Math.abs(host.velocity) < 0.004;\n      const hoverRect = rects[hover.index];\n      const hovering =\n        Math.abs(hoverGoal - hover.alpha) > 0.003 ||\n        (hoverRect && Math.abs(hoverRect.x + hoverRect.w / 2 - hover.x) + Math.abs(hoverRect.w - hover.w) > 0.1);\n      if (!resting || hovering) raf = requestAnimationFrame(frame);\n      else {\n        host.mass = 1;\n        host.scale = 1;\n        host.velocity = 0;\n        hover.alpha = hoverGoal;\n        render();\n      }\n    };\n\n    const wake = () => {\n      if (raf || disposed) return;\n      last = performance.now();\n      raf = requestAnimationFrame(frame);\n    };\n\n    const settle = (index: number) => {\n      pills.clear();\n      drops.length = 0;\n      target = index;\n      const state = pill(index);\n      state.mass = 1;\n      state.scale = 1;\n      state.velocity = 0;\n      render();\n    };\n\n    const go = (index: number, animate: boolean) => {\n      if (index === target) return;\n      if (!rects[index] || !animate || reduced || target < 0) {\n        settle(index);\n        return;\n      }\n      const now = performance.now();\n      target = index;\n      pill(index);\n      for (const drop of drops) {\n        drop.index = index;\n        if (drop.state === 'flying') plan(drop, index, now, false);\n        if (drop.state === 'absorbing') {\n          drop.state = 'flying';\n          drop.mass *= 1 - drop.absorbed;\n          drop.absorbed = 0;\n          plan(drop, index, now, false);\n        }\n      }\n      for (const from of [...pills.keys()]) {\n        if (from !== index) emit(from, index, now);\n      }\n      wake();\n    };\n\n    const onOver = (event: PointerEvent) => {\n      const item = (event.target as Element).closest<HTMLElement>('[data-gooey-index]');\n      if (!item || !list.contains(item)) return;\n      const index = Number(item.dataset.gooeyIndex);\n      const rect = rects[index];\n      if (!rect) return;\n      if (hover.alpha < 0.01) {\n        hover.x = rect.x + rect.w / 2;\n        hover.y = rect.y + rect.h / 2;\n        hover.w = rect.w;\n        hover.h = rect.h;\n      }\n      hover.index = index;\n      wake();\n    };\n\n    const onLeave = () => {\n      hover.index = -1;\n      wake();\n    };\n\n    const resizeObserver = new ResizeObserver(() => {\n      measure();\n      if (!drops.length) settle(target < 0 ? indexRef.current : target);\n      else render();\n    });\n    resizeObserver.observe(stage);\n    resizeObserver.observe(list);\n\n    list.addEventListener('pointerover', onOver);\n    list.addEventListener('pointerleave', onLeave);\n\n    measure();\n    settle(indexRef.current);\n    document.fonts?.ready.then(() => {\n      if (disposed) return;\n      measure();\n      if (!drops.length) settle(target);\n    });\n\n    engineRef.current = {\n      go: index => go(index, true),\n      refresh: () => {\n        render();\n        wake();\n      }\n    };\n\n    return () => {\n      disposed = true;\n      cancelAnimationFrame(raf);\n      resizeObserver.disconnect();\n      list.removeEventListener('pointerover', onOver);\n      list.removeEventListener('pointerleave', onLeave);\n      if (gl && pipeline) {\n        gl.deleteBuffer(pipeline.buffer);\n        gl.deleteProgram(pipeline.program);\n      }\n      engineRef.current = null;\n    };\n  }, [items.length]);\n\n  useEffect(() => {\n    engineRef.current?.go(current);\n  }, [current]);\n\n  useEffect(() => {\n    engineRef.current?.refresh();\n  }, [pillColor, palette, gooeyness, size, frame]);\n\n  const textColor = activeTextColor || contrastInk(pillRgb);\n\n  const labels = (interactive: boolean) =>\n    items.map((item, index) => {\n      const content = (\n        <>\n          {item.icon && <span className={ICON}>{item.icon}</span>}\n          {item.label && <span>{item.label}</span>}\n        </>\n      );\n      if (!interactive) {\n        return (\n          <li key={index} className=\"flex\">\n            <span className={GHOST}>{content}</span>\n          </li>\n        );\n      }\n      const shared = {\n        className: LINK,\n        'aria-current': index === current ? ('page' as const) : undefined,\n        onClick: (event: MouseEvent) => {\n          if (!item.href || item.href === '#') event.preventDefault();\n          select(index);\n        }\n      };\n      return (\n        <li\n          key={index}\n          className=\"group flex\"\n          data-gooey-index={index}\n          data-active={index === current ? '' : undefined}\n        >\n          {item.href ? (\n            <a href={item.href} {...shared}>\n              {content}\n            </a>\n          ) : (\n            <button type=\"button\" {...shared}>\n              {content}\n            </button>\n          )}\n        </li>\n      );\n    });\n\n  return (\n    <nav\n      className={`${ROOT}${frame ? ` ${FRAMED}` : ''} ${className}`.trim()}\n      style={\n        {\n          ...(SIZES[size] ?? SIZES.md),\n          ...THEMES[palette],\n          '--gn-pill': pillColor,\n          '--gn-active-ink': textColor,\n          ...style\n        } as CSSProperties\n      }\n    >\n      {frame && <span className={RIM} />}\n      <div ref={stageRef} className=\"relative max-w-full\">\n        <canvas ref={canvasRef} className=\"pointer-events-none absolute left-0 top-0\" aria-hidden=\"true\" />\n        <span\n          ref={fallbackRef}\n          className=\"pointer-events-none absolute left-0 top-0 hidden rounded-full bg-[var(--gn-pill)]\"\n          aria-hidden=\"true\"\n        />\n        <ul ref={listRef} className={LIST}>\n          {labels(true)}\n        </ul>\n        <ul ref={overlayRef} className={OVERLAY} aria-hidden=\"true\">\n          {labels(false)}\n        </ul>\n      </div>\n    </nav>\n  );\n};\n\nexport default GooeyNav;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": []
}