{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "animated-theme-toggler",
  "type": "registry:ui",
  "title": "Animated Theme Toggler",
  "author": "Rishabh Bose <https://theb0se.vercel.app/>",
  "description": "Ultra-smooth theme toggler utilizing the modern View Transitions API with fluid liquid, wave, and organic curve reveal transitions, viscous Apple-inspired easing, tactile micro-ripples, and a kinetic morphing SVG Sun/Moon icon.",
  "dependencies": [
    "framer-motion",
    "clsx",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "components/ui/animated-theme-toggler.tsx",
      "content": "\"use client\";\n\nimport React, {\n  useCallback,\n  useEffect,\n  useRef,\n  useState,\n  useId,\n} from \"react\";\nimport { flushSync } from \"react-dom\";\nimport { motion, useReducedMotion } from \"framer-motion\";\nimport { cn } from \"@/lib/utils\";\n\n/* ------------------------------------------------------------------ */\n/* Types & Available Transition Variants                              */\n/* ------------------------------------------------------------------ */\n\nexport type TransitionVariant =\n  // Organic Liquids, Waves & Curves (Primary)\n  | \"liquid-blob\"\n  | \"ocean-wave\"\n  | \"waterfall\"\n  | \"tide-sweep\"\n  | \"tide-sweep-left\"\n  | \"concentric-ripple\"\n  | \"liquid-droplet\"\n  | \"goo-melt\"\n  | \"aurora-wave\"\n  | \"bubble-burst\"\n  | \"fluid-curtain\"\n  | \"whirlpool\"\n  | \"circle\"\n  // Geometric & Directional (Legacy)\n  | \"diamond\"\n  | \"hexagon\"\n  | \"octagon\"\n  | \"star\"\n  | \"flower\"\n  | \"triangle\"\n  | \"square\"\n  | \"rectangle\"\n  | \"wipe-right\"\n  | \"wipe-left\"\n  | \"wipe-down\"\n  | \"wipe-up\"\n  | \"diagonal-slash\"\n  | \"curtain\"\n  | \"aperture\";\n\nexport interface AnimatedThemeTogglerProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n  /** Transition geometry variant. Default: \"liquid-blob\" */\n  variant?: TransitionVariant;\n  /** Transition duration in milliseconds. Default: 520 */\n  duration?: number;\n  /** Custom CSS easing timing function. Default: \"cubic-bezier(0.22, 1, 0.36, 1)\" (fluid deceleration) */\n  easing?: string;\n  /** When true, transition expands outward from viewport center instead of button center */\n  fromCenter?: boolean;\n  /** Controlled theme state ('light' | 'dark') */\n  theme?: \"light\" | \"dark\";\n  /** Callback fired whenever theme changes */\n  onThemeChange?: (theme: \"light\" | \"dark\") => void;\n  /** Custom size of toggle button */\n  size?: \"sm\" | \"md\" | \"lg\";\n  /** Whether to project a subtle ambient glow aura behind the button */\n  glow?: boolean;\n  /** Custom class name */\n  className?: string;\n}\n\n/* ------------------------------------------------------------------ */\n/* Geometry & Fluid View-Transition Clip-Path Generators               */\n/* ------------------------------------------------------------------ */\n\nfunction polygonCollapsed(pointStr: string, vertexCount: number): string {\n  const points = Array.from({ length: vertexCount }, () => pointStr).join(\", \");\n  return `polygon(${points})`;\n}\n\n/**\n * Computes responsive percentage-based clip-path keyframe tuples [startPath, midPath?, endPath].\n * Uses percentage calculations to ensure rock-solid scaling across Windows 125%/150% display zoom.\n */\nexport function getThemeTransitionClipPaths(\n  variant: TransitionVariant,\n  cx: number,\n  cy: number,\n  maxRadius: number,\n  viewportWidth: number,\n  viewportHeight: number\n): string[] {\n  const toX = (x: number) => `${((x / viewportWidth) * 100).toFixed(3)}%`;\n  const toY = (y: number) => `${((y / viewportHeight) * 100).toFixed(3)}%`;\n  const point = (x: number, y: number) => `${toX(x)} ${toY(y)}`;\n  const toRadius = (r: number) =>\n    `${((r / (Math.hypot(viewportWidth, viewportHeight) / Math.SQRT2)) * 100).toFixed(3)}%`;\n\n  switch (variant) {\n    /* ─────────────────────────────────────────────────────────────\n     * 1. LIQUID BLOB: Organic multi-lobed fluid bubble\n     * ───────────────────────────────────────────────────────────── */\n    case \"liquid-blob\": {\n      const N = 40;\n      const startVerts: string[] = [];\n      const midVerts: string[] = [];\n      const endVerts: string[] = [];\n\n      for (let i = 0; i < N; i++) {\n        const theta = (i * 2 * Math.PI) / N;\n        const cosT = Math.cos(theta);\n        const sinT = Math.sin(theta);\n\n        // Frame 0: Small fluid droplet collapsed at click center\n        const rStart = 4;\n        startVerts.push(point(cx + rStart * cosT, cy + rStart * sinT));\n\n        // Frame 1: Fluid multi-harmonic lobes wobbling with surface tension\n        const midLobe =\n          1 +\n          0.22 * Math.sin(3 * theta) +\n          0.14 * Math.cos(5 * theta + 0.8) +\n          0.08 * Math.sin(7 * theta);\n        const rMid = maxRadius * 0.65 * midLobe;\n        midVerts.push(point(cx + rMid * cosT, cy + rMid * sinT));\n\n        // Frame 2: Fully engulfing all viewport corners\n        const endLobe =\n          1 +\n          0.05 * Math.sin(3 * theta + Math.PI) +\n          0.03 * Math.cos(5 * theta);\n        const rEnd = maxRadius * 1.48 * endLobe;\n        endVerts.push(point(cx + rEnd * cosT, cy + rEnd * sinT));\n      }\n\n      return [\n        `polygon(${startVerts.join(\", \")})`,\n        `polygon(${midVerts.join(\", \")})`,\n        `polygon(${endVerts.join(\", \")})`,\n      ];\n    }\n\n    /* ─────────────────────────────────────────────────────────────\n     * 2. OCEAN WAVE: Sinusoidal liquid wave rising from bottom to top\n     * ───────────────────────────────────────────────────────────── */\n    case \"ocean-wave\": {\n      const steps = 36;\n      const startVerts: string[] = [];\n      const midVerts: string[] = [];\n      const endVerts: string[] = [];\n\n      for (let i = 0; i <= steps; i++) {\n        const u = i / steps;\n        const x = u * viewportWidth;\n\n        const yStart = viewportHeight * 1.08 + Math.sin(u * 4 * Math.PI) * 16;\n        const yMid =\n          viewportHeight * 0.48 +\n          Math.sin(u * 4 * Math.PI) * (viewportHeight * 0.07) +\n          Math.cos(u * 6 * Math.PI + 1.2) * (viewportHeight * 0.035);\n        const yEnd =\n          -viewportHeight * 0.12 + Math.sin(u * 4 * Math.PI + Math.PI) * 12;\n\n        startVerts.push(point(x, yStart));\n        midVerts.push(point(x, yMid));\n        endVerts.push(point(x, yEnd));\n      }\n\n      // Anchors enclosing the bottom half of the screen\n      const bRight = point(viewportWidth * 1.08, viewportHeight * 1.18);\n      const bLeft = point(-viewportWidth * 0.08, viewportHeight * 1.18);\n\n      startVerts.push(bRight, bLeft);\n      midVerts.push(bRight, bLeft);\n      endVerts.push(bRight, bLeft);\n\n      return [\n        `polygon(${startVerts.join(\", \")})`,\n        `polygon(${midVerts.join(\", \")})`,\n        `polygon(${endVerts.join(\", \")})`,\n      ];\n    }\n\n    /* ─────────────────────────────────────────────────────────────\n     * 3. WATERFALL: Cascading fluid curtain flowing from top to bottom\n     * ───────────────────────────────────────────────────────────── */\n    case \"waterfall\": {\n      const steps = 36;\n      const startVerts: string[] = [];\n      const midVerts: string[] = [];\n      const endVerts: string[] = [];\n\n      for (let i = 0; i <= steps; i++) {\n        const u = i / steps;\n        const x = u * viewportWidth;\n\n        const yStart =\n          -viewportHeight * 0.08 + Math.sin(u * 4 * Math.PI) * 16;\n        const yMid =\n          viewportHeight * 0.52 +\n          Math.sin(u * 4 * Math.PI) * (viewportHeight * 0.07) +\n          Math.cos(u * 7 * Math.PI + 0.8) * (viewportHeight * 0.035);\n        const yEnd =\n          viewportHeight * 1.15 + Math.sin(u * 4 * Math.PI + Math.PI) * 12;\n\n        startVerts.push(point(x, yStart));\n        midVerts.push(point(x, yMid));\n        endVerts.push(point(x, yEnd));\n      }\n\n      const tRight = point(viewportWidth * 1.08, -viewportHeight * 0.18);\n      const tLeft = point(-viewportWidth * 0.08, -viewportHeight * 0.18);\n\n      startVerts.push(tRight, tLeft);\n      midVerts.push(tRight, tLeft);\n      endVerts.push(tRight, tLeft);\n\n      return [\n        `polygon(${startVerts.join(\", \")})`,\n        `polygon(${midVerts.join(\", \")})`,\n        `polygon(${endVerts.join(\", \")})`,\n      ];\n    }\n\n    /* ─────────────────────────────────────────────────────────────\n     * 4. TIDE SWEEP: Rolling liquid wave surging left to right\n     * ───────────────────────────────────────────────────────────── */\n    case \"tide-sweep\": {\n      const steps = 36;\n      const startVerts: string[] = [];\n      const midVerts: string[] = [];\n      const endVerts: string[] = [];\n\n      for (let i = 0; i <= steps; i++) {\n        const v = i / steps;\n        const y = v * viewportHeight;\n\n        const xStart =\n          -viewportWidth * 0.08 + Math.sin(v * 4 * Math.PI) * 16;\n        const xMid =\n          viewportWidth * 0.50 +\n          Math.sin(v * 4 * Math.PI) * (viewportWidth * 0.07) +\n          Math.cos(v * 6 * Math.PI + 1.0) * (viewportWidth * 0.035);\n        const xEnd =\n          viewportWidth * 1.15 + Math.sin(v * 4 * Math.PI + Math.PI) * 12;\n\n        startVerts.push(point(xStart, y));\n        midVerts.push(point(xMid, y));\n        endVerts.push(point(xEnd, y));\n      }\n\n      const bLeft = point(-viewportWidth * 0.18, viewportHeight * 1.08);\n      const tLeft = point(-viewportWidth * 0.18, -viewportHeight * 0.08);\n\n      startVerts.push(bLeft, tLeft);\n      midVerts.push(bLeft, tLeft);\n      endVerts.push(bLeft, tLeft);\n\n      return [\n        `polygon(${startVerts.join(\", \")})`,\n        `polygon(${midVerts.join(\", \")})`,\n        `polygon(${endVerts.join(\", \")})`,\n      ];\n    }\n\n    /* ─────────────────────────────────────────────────────────────\n     * 5. TIDE SWEEP LEFT: Rolling liquid wave surging right to left\n     * ───────────────────────────────────────────────────────────── */\n    case \"tide-sweep-left\": {\n      const steps = 36;\n      const startVerts: string[] = [];\n      const midVerts: string[] = [];\n      const endVerts: string[] = [];\n\n      for (let i = 0; i <= steps; i++) {\n        const v = i / steps;\n        const y = v * viewportHeight;\n\n        const xStart =\n          viewportWidth * 1.08 + Math.sin(v * 4 * Math.PI) * 16;\n        const xMid =\n          viewportWidth * 0.50 -\n          Math.sin(v * 4 * Math.PI) * (viewportWidth * 0.07) -\n          Math.cos(v * 6 * Math.PI + 1.0) * (viewportWidth * 0.035);\n        const xEnd =\n          -viewportWidth * 0.15 - Math.sin(v * 4 * Math.PI + Math.PI) * 12;\n\n        startVerts.push(point(xStart, y));\n        midVerts.push(point(xMid, y));\n        endVerts.push(point(xEnd, y));\n      }\n\n      const bRight = point(viewportWidth * 1.18, viewportHeight * 1.08);\n      const tRight = point(viewportWidth * 1.18, -viewportHeight * 0.08);\n\n      startVerts.push(bRight, tRight);\n      midVerts.push(bRight, tRight);\n      endVerts.push(bRight, tRight);\n\n      return [\n        `polygon(${startVerts.join(\", \")})`,\n        `polygon(${midVerts.join(\", \")})`,\n        `polygon(${endVerts.join(\", \")})`,\n      ];\n    }\n\n    /* ─────────────────────────────────────────────────────────────\n     * 6. CONCENTRIC RIPPLE: Fluid multi-ring water ripple\n     * ───────────────────────────────────────────────────────────── */\n    case \"concentric-ripple\": {\n      const N = 40;\n      const startVerts: string[] = [];\n      const midVerts: string[] = [];\n      const endVerts: string[] = [];\n\n      for (let i = 0; i < N; i++) {\n        const theta = (i * 2 * Math.PI) / N;\n        const cosT = Math.cos(theta);\n        const sinT = Math.sin(theta);\n\n        const rStart = 4;\n        startVerts.push(point(cx + rStart * cosT, cy + rStart * sinT));\n\n        const midR = maxRadius * 0.62 * (1 + 0.16 * Math.sin(8 * theta));\n        midVerts.push(point(cx + midR * cosT, cy + midR * sinT));\n\n        const endR =\n          maxRadius * 1.48 * (1 + 0.03 * Math.sin(8 * theta + Math.PI));\n        endVerts.push(point(cx + endR * cosT, cy + endR * sinT));\n      }\n\n      return [\n        `polygon(${startVerts.join(\", \")})`,\n        `polygon(${midVerts.join(\", \")})`,\n        `polygon(${endVerts.join(\", \")})`,\n      ];\n    }\n\n    /* ─────────────────────────────────────────────────────────────\n     * 7. LIQUID DROPLET: Viscous teardrop splash\n     * ───────────────────────────────────────────────────────────── */\n    case \"liquid-droplet\": {\n      const N = 36;\n      const startVerts: string[] = [];\n      const midVerts: string[] = [];\n      const endVerts: string[] = [];\n\n      for (let i = 0; i < N; i++) {\n        const theta = (i * 2 * Math.PI) / N;\n        const cosT = Math.cos(theta);\n        const sinT = Math.sin(theta);\n\n        const rStart = 4;\n        startVerts.push(point(cx + rStart * cosT, cy + rStart * sinT));\n\n        const dropMid = 1 + 0.28 * sinT + 0.14 * Math.cos(2 * theta);\n        const rMid = maxRadius * 0.62 * dropMid;\n        midVerts.push(point(cx + rMid * cosT, cy + rMid * sinT));\n\n        const dropEnd = 1 + 0.05 * sinT;\n        const rEnd = maxRadius * 1.46 * dropEnd;\n        endVerts.push(point(cx + rEnd * cosT, cy + rEnd * sinT));\n      }\n\n      return [\n        `polygon(${startVerts.join(\", \")})`,\n        `polygon(${midVerts.join(\", \")})`,\n        `polygon(${endVerts.join(\", \")})`,\n      ];\n    }\n\n    /* ─────────────────────────────────────────────────────────────\n     * 8. GOO MELT: Organic melting slime with falling viscous droplets\n     * ───────────────────────────────────────────────────────────── */\n    case \"goo-melt\": {\n      const steps = 36;\n      const startVerts: string[] = [];\n      const midVerts: string[] = [];\n      const endVerts: string[] = [];\n\n      for (let i = 0; i <= steps; i++) {\n        const u = i / steps;\n        const x = u * viewportWidth;\n\n        const dripBase =\n          0.14 * Math.pow(Math.abs(Math.sin(u * 3 * Math.PI)), 3) +\n          0.18 * Math.pow(Math.abs(Math.sin(u * 5 * Math.PI + 0.6)), 4) +\n          0.06 * Math.sin(u * 2 * Math.PI);\n\n        const yStart = -viewportHeight * 0.08;\n        const yMid = viewportHeight * (0.35 + dripBase);\n        const yEnd = viewportHeight * (1.18 + dripBase * 0.15);\n\n        startVerts.push(point(x, yStart));\n        midVerts.push(point(x, yMid));\n        endVerts.push(point(x, yEnd));\n      }\n\n      const tRight = point(viewportWidth * 1.08, -viewportHeight * 0.18);\n      const tLeft = point(-viewportWidth * 0.08, -viewportHeight * 0.18);\n\n      startVerts.push(tRight, tLeft);\n      midVerts.push(tRight, tLeft);\n      endVerts.push(tRight, tLeft);\n\n      return [\n        `polygon(${startVerts.join(\", \")})`,\n        `polygon(${midVerts.join(\", \")})`,\n        `polygon(${endVerts.join(\", \")})`,\n      ];\n    }\n\n    /* ─────────────────────────────────────────────────────────────\n     * 9. AURORA WAVE: Flowing diagonal sinuous ribbon\n     * ───────────────────────────────────────────────────────────── */\n    case \"aurora-wave\": {\n      const steps = 36;\n      const startVerts: string[] = [];\n      const midVerts: string[] = [];\n      const endVerts: string[] = [];\n\n      for (let i = 0; i <= steps; i++) {\n        const u = i / steps;\n        const xBase = u * (viewportWidth * 1.4) - viewportWidth * 0.2;\n        const yBase = u * (viewportHeight * 1.4) - viewportHeight * 0.2;\n\n        const sinWave = Math.sin(u * 4 * Math.PI);\n        const amp = viewportHeight * 0.09;\n\n        const xWave = -sinWave * amp * 0.7;\n        const yWave = sinWave * amp;\n\n        startVerts.push(\n          point(-viewportWidth * 0.35 + xWave, -viewportHeight * 0.35 + yWave)\n        );\n        midVerts.push(point(xBase + xWave, yBase + yWave));\n        endVerts.push(\n          point(viewportWidth * 1.35 + xWave, viewportHeight * 1.35 + yWave)\n        );\n      }\n\n      const bLeft = point(-viewportWidth * 0.45, viewportHeight * 1.45);\n      const tRight = point(viewportWidth * 1.45, -viewportHeight * 0.45);\n\n      startVerts.push(bLeft, tRight);\n      midVerts.push(bLeft, tRight);\n      endVerts.push(bLeft, tRight);\n\n      return [\n        `polygon(${startVerts.join(\", \")})`,\n        `polygon(${midVerts.join(\", \")})`,\n        `polygon(${endVerts.join(\", \")})`,\n      ];\n    }\n\n    /* ─────────────────────────────────────────────────────────────\n     * 10. BUBBLE BURST: Smooth elastic sphere with surface tension\n     * ───────────────────────────────────────────────────────────── */\n    case \"bubble-burst\": {\n      const N = 36;\n      const startVerts: string[] = [];\n      const midVerts: string[] = [];\n      const endVerts: string[] = [];\n\n      for (let i = 0; i < N; i++) {\n        const theta = (i * 2 * Math.PI) / N;\n        const cosT = Math.cos(theta);\n        const sinT = Math.sin(theta);\n\n        const rStart = 3;\n        startVerts.push(point(cx + rStart * cosT, cy + rStart * sinT));\n\n        const bubbleMid =\n          1 + 0.08 * Math.sin(2 * theta) + 0.05 * Math.cos(4 * theta);\n        const rMid = maxRadius * 0.64 * bubbleMid;\n        midVerts.push(point(cx + rMid * cosT, cy + rMid * sinT));\n\n        const rEnd = maxRadius * 1.48;\n        endVerts.push(point(cx + rEnd * cosT, cy + rEnd * sinT));\n      }\n\n      return [\n        `polygon(${startVerts.join(\", \")})`,\n        `polygon(${midVerts.join(\", \")})`,\n        `polygon(${endVerts.join(\", \")})`,\n      ];\n    }\n\n    /* ─────────────────────────────────────────────────────────────\n     * 11. WHIRLPOOL: Swirling curved liquid eddy vortex\n     * ───────────────────────────────────────────────────────────── */\n    case \"whirlpool\": {\n      const N = 40;\n      const startVerts: string[] = [];\n      const midVerts: string[] = [];\n      const endVerts: string[] = [];\n\n      for (let i = 0; i < N; i++) {\n        const theta = (i * 2 * Math.PI) / N;\n\n        const rStart = 4;\n        startVerts.push(\n          point(cx + rStart * Math.cos(theta), cy + rStart * Math.sin(theta))\n        );\n\n        const phi = theta + 0.75 * Math.PI;\n        const rMid =\n          maxRadius * 0.65 * (1 + 0.22 * Math.sin(3 * theta + theta * 0.5));\n        midVerts.push(\n          point(cx + rMid * Math.cos(phi), cy + rMid * Math.sin(phi))\n        );\n\n        const endPhi = theta + 1.4 * Math.PI;\n        const rEnd = maxRadius * 1.48;\n        endVerts.push(\n          point(cx + rEnd * Math.cos(endPhi), cy + rEnd * Math.sin(endPhi))\n        );\n      }\n\n      return [\n        `polygon(${startVerts.join(\", \")})`,\n        `polygon(${midVerts.join(\", \")})`,\n        `polygon(${endVerts.join(\", \")})`,\n      ];\n    }\n\n    /* ─────────────────────────────────────────────────────────────\n     * 12. FLUID CURTAIN: Symmetrical liquid wave parting outward\n     * ───────────────────────────────────────────────────────────── */\n    case \"fluid-curtain\": {\n      const N = 40;\n      const startVerts: string[] = [];\n      const midVerts: string[] = [];\n      const endVerts: string[] = [];\n\n      for (let i = 0; i < N; i++) {\n        const theta = (i * 2 * Math.PI) / N;\n        const cosT = Math.cos(theta);\n        const sinT = Math.sin(theta);\n\n        const rStart = 4;\n        startVerts.push(point(cx + rStart * cosT, cy + rStart * sinT));\n\n        const hScale = 1.7;\n        const rMid =\n          maxRadius * 0.65 * (1 + 0.15 * Math.sin(6 * theta));\n        midVerts.push(point(cx + rMid * cosT * hScale, cy + rMid * sinT));\n\n        const rEnd = maxRadius * 1.5;\n        endVerts.push(point(cx + rEnd * cosT * hScale, cy + rEnd * sinT));\n      }\n\n      return [\n        `polygon(${startVerts.join(\", \")})`,\n        `polygon(${midVerts.join(\", \")})`,\n        `polygon(${endVerts.join(\", \")})`,\n      ];\n    }\n\n    /* ─────────────────────────────────────────────────────────────\n     * 13. CIRCLE: Smooth radial curve\n     * ───────────────────────────────────────────────────────────── */\n    case \"circle\": {\n      return [\n        `circle(0% at ${point(cx, cy)})`,\n        `circle(${toRadius(maxRadius * 0.58)} at ${point(cx, cy)})`,\n        `circle(${toRadius(maxRadius * 1.08)} at ${point(cx, cy)})`,\n      ];\n    }\n\n    /* ─────────────────────────────────────────────────────────────\n     * Backward-Compatible Geometric & Directional Variants\n     * ───────────────────────────────────────────────────────────── */\n    case \"diamond\": {\n      const R = maxRadius * Math.SQRT2 * 1.05;\n      const end = [\n        point(cx, cy - R),\n        point(cx + R, cy),\n        point(cx, cy + R),\n        point(cx - R, cy),\n      ].join(\", \");\n      return [polygonCollapsed(point(cx, cy), 4), `polygon(${end})`];\n    }\n\n    case \"square\": {\n      const halfSide =\n        Math.max(\n          Math.max(cx, viewportWidth - cx),\n          Math.max(cy, viewportHeight - cy)\n        ) * 1.05;\n      const end = [\n        point(cx - halfSide, cy - halfSide),\n        point(cx + halfSide, cy - halfSide),\n        point(cx + halfSide, cy + halfSide),\n        point(cx - halfSide, cy + halfSide),\n      ].join(\", \");\n      return [polygonCollapsed(point(cx, cy), 4), `polygon(${end})`];\n    }\n\n    case \"rectangle\": {\n      const halfW = Math.max(cx, viewportWidth - cx) * 1.05;\n      const halfH = Math.max(cy, viewportHeight - cy) * 1.05;\n      const end = [\n        point(cx - halfW, cy - halfH),\n        point(cx + halfW, cy - halfH),\n        point(cx + halfW, cy + halfH),\n        point(cx - halfW, cy + halfH),\n      ].join(\", \");\n      return [polygonCollapsed(point(cx, cy), 4), `polygon(${end})`];\n    }\n\n    case \"triangle\": {\n      const scale = maxRadius * 2.3;\n      const dx = (Math.sqrt(3) / 2) * scale;\n      const verts = [\n        point(cx, cy - scale),\n        point(cx + dx, cy + 0.5 * scale),\n        point(cx - dx, cy + 0.5 * scale),\n      ].join(\", \");\n      return [polygonCollapsed(point(cx, cy), 3), `polygon(${verts})`];\n    }\n\n    case \"hexagon\": {\n      const R = maxRadius * Math.SQRT2 * 1.05;\n      const verts: string[] = [];\n      for (let i = 0; i < 6; i++) {\n        const a = -Math.PI / 2 + (i * Math.PI) / 3;\n        verts.push(point(cx + R * Math.cos(a), cy + R * Math.sin(a)));\n      }\n      return [polygonCollapsed(point(cx, cy), 6), `polygon(${verts.join(\", \")})`];\n    }\n\n    case \"octagon\": {\n      const R = maxRadius * Math.SQRT2 * 1.05;\n      const verts: string[] = [];\n      for (let i = 0; i < 8; i++) {\n        const a = -Math.PI / 2 + (i * Math.PI) / 4;\n        verts.push(point(cx + R * Math.cos(a), cy + R * Math.sin(a)));\n      }\n      return [polygonCollapsed(point(cx, cy), 8), `polygon(${verts.join(\", \")})`];\n    }\n\n    case \"star\": {\n      const R = maxRadius * Math.SQRT2 * 1.06;\n      const innerRatio = 0.42;\n      const starPoly = (radius: number) => {\n        const verts: string[] = [];\n        for (let i = 0; i < 5; i++) {\n          const outerA = -Math.PI / 2 + (i * 2 * Math.PI) / 5;\n          verts.push(\n            point(cx + radius * Math.cos(outerA), cy + radius * Math.sin(outerA))\n          );\n          const innerA = outerA + Math.PI / 5;\n          verts.push(\n            point(\n              cx + radius * innerRatio * Math.cos(innerA),\n              cy + radius * innerRatio * Math.sin(innerA)\n            )\n          );\n        }\n        return `polygon(${verts.join(\", \")})`;\n      };\n      return [starPoly(3), starPoly(R)];\n    }\n\n    case \"flower\": {\n      const R = maxRadius * Math.SQRT2 * 1.06;\n      const innerRatio = 0.62;\n      const flowerPoly = (radius: number) => {\n        const verts: string[] = [];\n        for (let i = 0; i < 8; i++) {\n          const outerA = (i * 2 * Math.PI) / 8;\n          verts.push(\n            point(cx + radius * Math.cos(outerA), cy + radius * Math.sin(outerA))\n          );\n          const innerA = outerA + Math.PI / 8;\n          verts.push(\n            point(\n              cx + radius * innerRatio * Math.cos(innerA),\n              cy + radius * innerRatio * Math.sin(innerA)\n            )\n          );\n        }\n        return `polygon(${verts.join(\", \")})`;\n      };\n      return [flowerPoly(3), flowerPoly(R)];\n    }\n\n    case \"aperture\": {\n      const R = maxRadius * Math.SQRT2 * 1.08;\n      const N = 12;\n      const irisPoly = (radius: number, swirl: number) => {\n        const verts: string[] = [];\n        for (let i = 0; i < N; i++) {\n          const a = (i * 2 * Math.PI) / N + swirl;\n          verts.push(point(cx + radius * Math.cos(a), cy + radius * Math.sin(a)));\n        }\n        return `polygon(${verts.join(\", \")})`;\n      };\n      return [irisPoly(3, 0), irisPoly(R, Math.PI / 3)];\n    }\n\n    case \"wipe-right\": {\n      const start = [\n        point(0, 0),\n        point(0, 0),\n        point(0, viewportHeight),\n        point(0, viewportHeight),\n      ].join(\", \");\n      const end = [\n        point(0, 0),\n        point(viewportWidth, 0),\n        point(viewportWidth, viewportHeight),\n        point(0, viewportHeight),\n      ].join(\", \");\n      return [`polygon(${start})`, `polygon(${end})`];\n    }\n\n    case \"wipe-left\": {\n      const start = [\n        point(viewportWidth, 0),\n        point(viewportWidth, 0),\n        point(viewportWidth, viewportHeight),\n        point(viewportWidth, viewportHeight),\n      ].join(\", \");\n      const end = [\n        point(viewportWidth, 0),\n        point(0, 0),\n        point(0, viewportHeight),\n        point(viewportWidth, viewportHeight),\n      ].join(\", \");\n      return [`polygon(${start})`, `polygon(${end})`];\n    }\n\n    case \"wipe-down\": {\n      const start = [\n        point(0, 0),\n        point(viewportWidth, 0),\n        point(viewportWidth, 0),\n        point(0, 0),\n      ].join(\", \");\n      const end = [\n        point(0, 0),\n        point(viewportWidth, 0),\n        point(viewportWidth, viewportHeight),\n        point(0, viewportHeight),\n      ].join(\", \");\n      return [`polygon(${start})`, `polygon(${end})`];\n    }\n\n    case \"wipe-up\": {\n      const start = [\n        point(0, viewportHeight),\n        point(viewportWidth, viewportHeight),\n        point(viewportWidth, viewportHeight),\n        point(0, viewportHeight),\n      ].join(\", \");\n      const end = [\n        point(0, viewportHeight),\n        point(viewportWidth, viewportHeight),\n        point(viewportWidth, 0),\n        point(0, 0),\n      ].join(\", \");\n      return [`polygon(${start})`, `polygon(${end})`];\n    }\n\n    case \"diagonal-slash\": {\n      const maxDim = Math.max(viewportWidth, viewportHeight) * 2.2;\n      const start = [\n        point(0, 0),\n        point(0, 0),\n        point(0, 0),\n        point(0, 0),\n      ].join(\", \");\n      const end = [\n        point(-maxDim * 0.2, -maxDim * 0.2),\n        point(maxDim, -maxDim * 0.2),\n        point(maxDim * 0.2, maxDim),\n        point(-maxDim * 0.2, maxDim),\n      ].join(\", \");\n      return [`polygon(${start})`, `polygon(${end})`];\n    }\n\n    case \"curtain\": {\n      const midX = viewportWidth / 2;\n      const start = [\n        point(midX, 0),\n        point(midX, 0),\n        point(midX, viewportHeight),\n        point(midX, viewportHeight),\n      ].join(\", \");\n      const end = [\n        point(0, 0),\n        point(viewportWidth, 0),\n        point(viewportWidth, viewportHeight),\n        point(0, viewportHeight),\n      ].join(\", \");\n      return [`polygon(${start})`, `polygon(${end})`];\n    }\n\n    default:\n      return [\n        `circle(0% at ${point(cx, cy)})`,\n        `circle(${toRadius(maxRadius * 0.58)} at ${point(cx, cy)})`,\n        `circle(${toRadius(maxRadius * 1.08)} at ${point(cx, cy)})`,\n      ];\n  }\n}\n\n/* ------------------------------------------------------------------ */\n/* Kinetic Morphing Sun/Moon SVG Icon Component                       */\n/* ------------------------------------------------------------------ */\n\ninterface MorphingThemeIconProps {\n  isDark: boolean;\n  className?: string;\n}\n\nexport function MorphingThemeIcon({ isDark, className }: MorphingThemeIconProps) {\n  const maskId = useId();\n\n  return (\n    <motion.svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      className={cn(\"h-5 w-5 pointer-events-none select-none\", className)}\n      animate={{\n        rotate: isDark ? 40 : 0,\n      }}\n      transition={{\n        type: \"spring\",\n        stiffness: 260,\n        damping: 20,\n      }}\n    >\n      <defs>\n        <mask id={maskId}>\n          <rect x=\"0\" y=\"0\" width=\"100%\" height=\"100%\" fill=\"white\" />\n          {/* Animated eclipse circle carving the moon crescent */}\n          <motion.circle\n            animate={{\n              cx: isDark ? 9 : 28,\n              cy: isDark ? 5 : -4,\n              r: isDark ? 8.2 : 0,\n            }}\n            transition={{\n              type: \"spring\",\n              stiffness: 300,\n              damping: 24,\n            }}\n            fill=\"black\"\n          />\n        </mask>\n      </defs>\n\n      {/* Sun / Moon Core Disc */}\n      <motion.circle\n        cx=\"12\"\n        cy=\"12\"\n        fill=\"currentColor\"\n        mask={`url(#${maskId})`}\n        animate={{\n          r: isDark ? 8 : 5,\n        }}\n        transition={{\n          type: \"spring\",\n          stiffness: 320,\n          damping: 22,\n        }}\n      />\n\n      {/* Radiant Sun Rays (pop out when light, tuck away when dark) */}\n      <motion.g\n        stroke=\"currentColor\"\n        animate={{\n          opacity: isDark ? 0 : 1,\n          scale: isDark ? 0.35 : 1,\n        }}\n        transition={{\n          type: \"spring\",\n          stiffness: 340,\n          damping: 20,\n        }}\n        style={{ originX: \"12px\", originY: \"12px\" }}\n      >\n        <line x1=\"12\" y1=\"1\" x2=\"12\" y2=\"3\" />\n        <line x1=\"12\" y1=\"21\" x2=\"12\" y2=\"23\" />\n        <line x1=\"4.22\" y1=\"4.22\" x2=\"5.64\" y2=\"5.64\" />\n        <line x1=\"18.36\" y1=\"18.36\" x2=\"19.78\" y2=\"19.78\" />\n        <line x1=\"1\" y1=\"12\" x2=\"3\" y2=\"12\" />\n        <line x1=\"21\" y1=\"12\" x2=\"23\" y2=\"12\" />\n        <line x1=\"4.22\" y1=\"19.78\" x2=\"5.64\" y2=\"18.36\" />\n        <line x1=\"18.36\" y1=\"5.64\" x2=\"19.78\" y2=\"4.22\" />\n      </motion.g>\n\n      {/* Twinkling Night Stars */}\n      <motion.circle\n        animate={{\n          opacity: isDark ? 1 : 0,\n          scale: isDark ? 1 : 0,\n        }}\n        cx=\"19\"\n        cy=\"15\"\n        r=\"1\"\n        fill=\"currentColor\"\n        className=\"text-neutral-300 dark:text-neutral-200\"\n        transition={{ type: \"spring\", stiffness: 380, damping: 22, delay: 0.04 }}\n      />\n      <motion.circle\n        animate={{\n          opacity: isDark ? 0.75 : 0,\n          scale: isDark ? 1 : 0,\n        }}\n        cx=\"16\"\n        cy=\"20\"\n        r=\"0.75\"\n        fill=\"currentColor\"\n        className=\"text-neutral-400 dark:text-neutral-300\"\n        transition={{ type: \"spring\", stiffness: 380, damping: 22, delay: 0.08 }}\n      />\n    </motion.svg>\n  );\n}\n\n/* ------------------------------------------------------------------ */\n/* Main AnimatedThemeToggler Component                                */\n/* ------------------------------------------------------------------ */\n\nexport const AnimatedThemeToggler = ({\n  className,\n  duration = 520,\n  easing = \"cubic-bezier(0.22, 1, 0.36, 1)\",\n  variant = \"liquid-blob\",\n  fromCenter = false,\n  theme,\n  onThemeChange,\n  size = \"md\",\n  glow = true,\n  ...props\n}: AnimatedThemeTogglerProps) => {\n  const isControlled = theme !== undefined;\n  const [internalIsDark, setInternalIsDark] = useState(false);\n  const isDark = isControlled ? theme === \"dark\" : internalIsDark;\n  const buttonRef = useRef<HTMLButtonElement>(null);\n  const isTransitioningRef = useRef(false);\n  const activeAnimRef = useRef<Animation | null>(null);\n  const shouldReduceMotion = useReducedMotion();\n\n  // Internal tactile liquid ripples inside button\n  const [internalRipples, setInternalRipples] = useState<\n    { id: number; x: number; y: number }[]\n  >([]);\n\n  const cancelAnim = useCallback(() => {\n    activeAnimRef.current?.cancel();\n    activeAnimRef.current = null;\n  }, []);\n\n  // Ensure View Transition clean clipping rules are injected in DOM\n  useEffect(() => {\n    const STYLE_ID = \"boseui-vt-clean-styles\";\n    if (typeof document !== \"undefined\" && !document.getElementById(STYLE_ID)) {\n      const style = document.createElement(\"style\");\n      style.id = STYLE_ID;\n      style.innerHTML = `\n        ::view-transition-old(root),\n        ::view-transition-new(root) {\n          animation: none !important;\n          mix-blend-mode: normal !important;\n        }\n      `;\n      document.head.appendChild(style);\n    }\n\n    return () => {\n      cancelAnim();\n      const root = document.documentElement;\n      delete root.dataset.boseuiThemeVt;\n    };\n  }, [cancelAnim]);\n\n  // Synchronize internal state with document dark class for uncontrolled usage\n  useEffect(() => {\n    if (isControlled) return;\n\n    const updateTheme = () => {\n      setInternalIsDark(document.documentElement.classList.contains(\"dark\"));\n    };\n\n    updateTheme();\n\n    const observer = new MutationObserver(updateTheme);\n    observer.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n\n    return () => observer.disconnect();\n  }, [isControlled]);\n\n  const toggleTheme = useCallback(\n    (e: React.MouseEvent<HTMLButtonElement>) => {\n      const button = buttonRef.current;\n      if (!button) return;\n\n      // Spawn internal micro liquid ripple inside button\n      const rect = button.getBoundingClientRect();\n      const clickX = (e.clientX || rect.left + rect.width / 2) - rect.left;\n      const clickY = (e.clientY || rect.top + rect.height / 2) - rect.top;\n      setInternalRipples((prev) => [\n        ...prev.slice(-2),\n        { id: Date.now(), x: clickX, y: clickY },\n      ]);\n\n      if (\n        isTransitioningRef.current ||\n        document.documentElement.dataset.boseuiThemeVt === \"active\"\n      ) {\n        return;\n      }\n\n      const viewportWidth = window.innerWidth;\n      const viewportHeight = window.innerHeight;\n\n      let x: number;\n      let y: number;\n\n      if (fromCenter) {\n        x = viewportWidth / 2;\n        y = viewportHeight / 2;\n      } else if (e.clientX && e.clientY) {\n        x = e.clientX;\n        y = e.clientY;\n      } else {\n        x = rect.left + rect.width / 2;\n        y = rect.top + rect.height / 2;\n      }\n\n      const maxRadius = Math.hypot(\n        Math.max(x, viewportWidth - x),\n        Math.max(y, viewportHeight - y)\n      );\n\n      const applyTheme = () => {\n        const newTheme = !isDark;\n        document.documentElement.classList.toggle(\"dark\");\n        if (newTheme) {\n          document.documentElement.style.colorScheme = \"dark\";\n        } else {\n          document.documentElement.style.colorScheme = \"light\";\n        }\n\n        if (isControlled) {\n          onThemeChange?.(newTheme ? \"dark\" : \"light\");\n        } else {\n          setInternalIsDark(newTheme);\n          window.localStorage.setItem(\"boseui-theme\", newTheme ? \"dark\" : \"light\");\n        }\n      };\n\n      // Fallback for browsers without View Transitions API or prefers-reduced-motion\n      if (\n        typeof document.startViewTransition !== \"function\" ||\n        shouldReduceMotion\n      ) {\n        applyTheme();\n        return;\n      }\n\n      const clipPath = getThemeTransitionClipPaths(\n        variant,\n        x,\n        y,\n        maxRadius,\n        viewportWidth,\n        viewportHeight\n      );\n\n      const root = document.documentElement;\n      root.dataset.boseuiThemeVt = \"active\";\n\n      const cleanup = () => {\n        isTransitioningRef.current = false;\n        delete root.dataset.boseuiThemeVt;\n        cancelAnim();\n      };\n\n      isTransitioningRef.current = true;\n      const transition = document.startViewTransition(() => {\n        flushSync(applyTheme);\n      });\n\n      if (typeof transition?.finished?.finally === \"function\") {\n        transition.finished.finally(cleanup).catch(() => {});\n      } else {\n        cleanup();\n      }\n\n      const ready = transition?.ready;\n      if (ready && typeof ready.then === \"function\") {\n        ready\n          .then(() => {\n            const anim = document.documentElement.animate(\n              {\n                clipPath,\n              },\n              {\n                duration,\n                easing,\n                fill: \"forwards\",\n                pseudoElement: \"::view-transition-new(root)\",\n              }\n            );\n            activeAnimRef.current = anim;\n          })\n          .catch(() => {});\n      }\n    },\n    [\n      variant,\n      fromCenter,\n      duration,\n      easing,\n      isDark,\n      isControlled,\n      onThemeChange,\n      cancelAnim,\n      shouldReduceMotion,\n    ]\n  );\n\n  const sizeClasses = {\n    sm: \"h-8 w-8\",\n    md: \"h-9 w-9\",\n    lg: \"h-11 w-11\",\n  }[size];\n\n  return (\n    <motion.button\n      type=\"button\"\n      ref={buttonRef}\n      onClick={toggleTheme}\n      whileHover={{ scale: 1.06 }}\n      whileTap={{ scale: 0.90, rotate: isDark ? -8 : 8 }}\n      transition={{\n        type: \"spring\",\n        stiffness: 450,\n        damping: 16,\n      }}\n      aria-label={`Switch to ${isDark ? \"light\" : \"dark\"} mode`}\n      title={`Switch to ${isDark ? \"light\" : \"dark\"} mode (${variant} transition)`}\n      className={cn(\n        \"relative flex items-center justify-center rounded-full border transition-all overflow-hidden\",\n        \"border-neutral-200/90 bg-white/90 text-neutral-800 shadow-2xs backdrop-blur-md hover:border-neutral-300 hover:text-neutral-950\",\n        \"dark:border-neutral-800/90 dark:bg-neutral-900/90 dark:text-neutral-100 dark:hover:border-neutral-700 dark:hover:text-white\",\n        \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-black\",\n        sizeClasses,\n        className\n      )}\n      {...(props as any)}\n    >\n      {/* Ambient theme aura glow */}\n      {glow && (\n        <span\n          aria-hidden=\"true\"\n          className={cn(\n            \"pointer-events-none absolute -inset-1 rounded-full opacity-60 blur-xs transition-opacity duration-300\",\n            isDark ? \"bg-indigo-500/15\" : \"bg-amber-500/15\"\n          )}\n        />\n      )}\n\n      {/* Internal tactile liquid ripple */}\n      <div className=\"pointer-events-none absolute inset-0 overflow-hidden rounded-full\">\n        {internalRipples.map((rip) => (\n          <motion.span\n            key={rip.id}\n            initial={{ scale: 0, opacity: 0.55 }}\n            animate={{ scale: 3.2, opacity: 0 }}\n            transition={{ duration: 0.55, ease: \"easeOut\" }}\n            style={{ left: rip.x, top: rip.y }}\n            className={cn(\n              \"absolute h-8 w-8 -translate-x-1/2 -translate-y-1/2 rounded-full blur-xs\",\n              isDark ? \"bg-amber-400/35\" : \"bg-indigo-500/25\"\n            )}\n          />\n        ))}\n      </div>\n\n      <MorphingThemeIcon isDark={isDark} />\n      <span className=\"sr-only\">Toggle theme</span>\n    </motion.button>\n  );\n};\n\nexport default AnimatedThemeToggler;\n",
      "type": "registry:ui",
      "target": "components/ui/animated-theme-toggler.tsx"
    },
    {
      "path": "lib/utils.ts",
      "content": "import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\nexport async function copyToClipboard(text: string): Promise<boolean> {\n  if (typeof window === \"undefined\") return false;\n\n  try {\n    if (navigator.clipboard && window.isSecureContext) {\n      await navigator.clipboard.writeText(text);\n      return true;\n    }\n\n    const textArea = document.createElement(\"textarea\");\n    textArea.value = text;\n    textArea.style.position = \"fixed\";\n    textArea.style.left = \"-999999px\";\n    textArea.style.top = \"-999999px\";\n    document.body.appendChild(textArea);\n    textArea.focus();\n    textArea.select();\n    const succeeded = document.execCommand(\"copy\");\n    textArea.remove();\n    return succeeded;\n  } catch {\n    return false;\n  }\n}\n",
      "type": "registry:lib",
      "target": "lib/utils.ts"
    }
  ]
}
