{"version":1,"id":"orbit","framework":"next","kind":"block","files":[{"path":"src/blocks/gfa/orbit.tsx","content":"\"use client\";\n\nimport type { CSSProperties } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst SIZE_THRESHOLD_SMALL = 50;\nconst SIZE_THRESHOLD_TINY = 30;\nconst SIZE_THRESHOLD_MEDIUM = 100;\nconst BLUR_MULTIPLIER_SMALL = 0.008;\nconst BLUR_MIN_SMALL = 1;\nconst BLUR_MULTIPLIER_LARGE = 0.015;\nconst BLUR_MIN_LARGE = 4;\nconst CONTRAST_MULTIPLIER_SMALL = 0.004;\nconst CONTRAST_MIN_SMALL = 1.2;\nconst CONTRAST_MULTIPLIER_LARGE = 0.008;\nconst CONTRAST_MIN_LARGE = 1.5;\nconst DOT_SIZE_MULTIPLIER_SMALL = 0.004;\nconst DOT_SIZE_MIN_SMALL = 0.05;\nconst DOT_SIZE_MULTIPLIER_LARGE = 0.008;\nconst DOT_SIZE_MIN_LARGE = 0.1;\nconst SHADOW_MULTIPLIER_SMALL = 0.004;\nconst SHADOW_MIN_SMALL = 0.5;\nconst SHADOW_MULTIPLIER_LARGE = 0.008;\nconst SHADOW_MIN_LARGE = 2;\nconst MASK_RADIUS_TINY = \"0%\";\nconst MASK_RADIUS_SMALL = \"5%\";\nconst MASK_RADIUS_MEDIUM = \"15%\";\nconst MASK_RADIUS_LARGE = \"25%\";\nconst CONTRAST_TINY = 1.1;\nconst CONTRAST_MULTIPLIER_FINAL = 1.2;\nconst CONTRAST_MIN_FINAL = 1.3;\n\n/** Animated orbit (ported from buss `smoothui/siri-orb`). */\nexport type OrbitProps = {\n  animationDuration?: number;\n  className?: string;\n  colors?: {\n    bg?: string;\n    c1?: string;\n    c2?: string;\n    c3?: string;\n  };\n  size?: string;\n};\n\n/** Dark chip bg — Ask AI / chat loading mark. */\nexport const ORBIT_DARK_COLORS = {\n  bg: \"oklch(22.64% 0 0)\",\n} as const;\n\n/** @deprecated Use ORBIT_DARK_COLORS */\nexport const ORBIT_KOPRU_COLORS = ORBIT_DARK_COLORS;\n\nexport function Orbit({\n  size = \"192px\",\n  className,\n  colors,\n  animationDuration = 20,\n}: OrbitProps) {\n  const defaultColors = {\n    bg: \"oklch(95% 0.02 264.695)\",\n    c1: \"oklch(75% 0.15 350)\",\n    c2: \"oklch(80% 0.12 200)\",\n    c3: \"oklch(78% 0.14 280)\",\n  };\n\n  const finalColors = { ...defaultColors, ...colors };\n  const sizeValue = Number.parseInt(size.replace(\"px\", \"\"), 10) || 192;\n\n  const blurAmount =\n    sizeValue < SIZE_THRESHOLD_SMALL\n      ? Math.max(sizeValue * BLUR_MULTIPLIER_SMALL, BLUR_MIN_SMALL)\n      : Math.max(sizeValue * BLUR_MULTIPLIER_LARGE, BLUR_MIN_LARGE);\n\n  const contrastAmount =\n    sizeValue < SIZE_THRESHOLD_SMALL\n      ? Math.max(sizeValue * CONTRAST_MULTIPLIER_SMALL, CONTRAST_MIN_SMALL)\n      : Math.max(sizeValue * CONTRAST_MULTIPLIER_LARGE, CONTRAST_MIN_LARGE);\n\n  const dotSize =\n    sizeValue < SIZE_THRESHOLD_SMALL\n      ? Math.max(sizeValue * DOT_SIZE_MULTIPLIER_SMALL, DOT_SIZE_MIN_SMALL)\n      : Math.max(sizeValue * DOT_SIZE_MULTIPLIER_LARGE, DOT_SIZE_MIN_LARGE);\n\n  const shadowSpread =\n    sizeValue < SIZE_THRESHOLD_SMALL\n      ? Math.max(sizeValue * SHADOW_MULTIPLIER_SMALL, SHADOW_MIN_SMALL)\n      : Math.max(sizeValue * SHADOW_MULTIPLIER_LARGE, SHADOW_MIN_LARGE);\n\n  const getMaskRadius = (value: number) => {\n    if (value < SIZE_THRESHOLD_TINY) return MASK_RADIUS_TINY;\n    if (value < SIZE_THRESHOLD_SMALL) return MASK_RADIUS_SMALL;\n    if (value < SIZE_THRESHOLD_MEDIUM) return MASK_RADIUS_MEDIUM;\n    return MASK_RADIUS_LARGE;\n  };\n\n  const maskRadius = getMaskRadius(sizeValue);\n\n  const getFinalContrast = (value: number) => {\n    if (value < SIZE_THRESHOLD_TINY) return CONTRAST_TINY;\n    if (value < SIZE_THRESHOLD_SMALL) {\n      return Math.max(\n        contrastAmount * CONTRAST_MULTIPLIER_FINAL,\n        CONTRAST_MIN_FINAL,\n      );\n    }\n    return contrastAmount;\n  };\n\n  const finalContrast = getFinalContrast(sizeValue);\n\n  return (\n    <div\n      data-slot=\"orbit\"\n      className={cn(\"gfa-orbit\", className)}\n      style={\n        {\n          width: size,\n          height: size,\n          \"--bg\": finalColors.bg,\n          \"--c1\": finalColors.c1,\n          \"--c2\": finalColors.c2,\n          \"--c3\": finalColors.c3,\n          \"--animation-duration\": `${animationDuration}s`,\n          \"--blur-amount\": `${blurAmount}px`,\n          \"--contrast-amount\": finalContrast,\n          \"--dot-size\": `${dotSize}px`,\n          \"--shadow-spread\": `${shadowSpread}px`,\n          \"--mask-radius\": maskRadius,\n        } as CSSProperties\n      }\n    >\n      <style>{`\n        @property --angle {\n          syntax: \"<angle>\";\n          inherits: false;\n          initial-value: 0deg;\n        }\n\n        .gfa-orbit {\n          display: grid;\n          grid-template-areas: \"stack\";\n          overflow: hidden;\n          border-radius: 50%;\n          position: relative;\n        }\n\n        .gfa-orbit::before,\n        .gfa-orbit::after {\n          content: \"\";\n          display: block;\n          grid-area: stack;\n          width: 100%;\n          height: 100%;\n          border-radius: 50%;\n        }\n\n        .gfa-orbit::before {\n          background:\n            conic-gradient(\n              from calc(var(--angle) * 2) at 25% 70%,\n              var(--c3),\n              transparent 20% 80%,\n              var(--c3)\n            ),\n            conic-gradient(\n              from calc(var(--angle) * 2) at 45% 75%,\n              var(--c2),\n              transparent 30% 60%,\n              var(--c2)\n            ),\n            conic-gradient(\n              from calc(var(--angle) * -3) at 80% 20%,\n              var(--c1),\n              transparent 40% 60%,\n              var(--c1)\n            ),\n            conic-gradient(\n              from calc(var(--angle) * 2) at 15% 5%,\n              var(--c2),\n              transparent 10% 90%,\n              var(--c2)\n            ),\n            conic-gradient(\n              from calc(var(--angle) * 1) at 20% 80%,\n              var(--c1),\n              transparent 10% 90%,\n              var(--c1)\n            ),\n            conic-gradient(\n              from calc(var(--angle) * -2) at 85% 10%,\n              var(--c3),\n              transparent 20% 80%,\n              var(--c3)\n            );\n          box-shadow: inset var(--bg) 0 0 var(--shadow-spread)\n            calc(var(--shadow-spread) * 0.2);\n          filter: blur(var(--blur-amount)) contrast(var(--contrast-amount));\n          animation: gfa-orbit-rotate var(--animation-duration) linear infinite;\n        }\n\n        .gfa-orbit::after {\n          background-image: radial-gradient(\n            circle at center,\n            var(--bg) var(--dot-size),\n            transparent var(--dot-size)\n          );\n          background-size: calc(var(--dot-size) * 2) calc(var(--dot-size) * 2);\n          backdrop-filter: blur(calc(var(--blur-amount) * 2))\n            contrast(calc(var(--contrast-amount) * 2));\n          mix-blend-mode: overlay;\n        }\n\n        .gfa-orbit[style*=\"--mask-radius: 0%\"]::after {\n          mask-image: none;\n        }\n\n        .gfa-orbit:not([style*=\"--mask-radius: 0%\"])::after {\n          mask-image: radial-gradient(\n            black var(--mask-radius),\n            transparent 75%\n          );\n        }\n\n        @keyframes gfa-orbit-rotate {\n          to {\n            --angle: 360deg;\n          }\n        }\n\n        @media (prefers-reduced-motion: reduce) {\n          .gfa-orbit::before {\n            animation: none;\n          }\n        }\n      `}</style>\n    </div>\n  );\n}\n\nexport function AskAiButton({\n  label = \"Ask AI\",\n  className,\n  onClick,\n}: {\n  label?: string;\n  className?: string;\n  onClick?: () => void;\n}) {\n  return (\n    <button\n      type=\"button\"\n      onClick={onClick}\n      className={cn(\n        \"inline-flex h-10 items-center gap-2 rounded-xl border border-black/10 bg-black px-3 text-sm font-semibold text-white/85 shadow-sm transition hover:bg-black/90\",\n        \"dark:border-white/10\",\n        className,\n      )}\n    >\n      <Orbit size=\"24px\" colors={ORBIT_DARK_COLORS} className=\"shrink-0\" />\n      <span>{label}</span>\n    </button>\n  );\n}\n"}],"meta":{"studioName":"GFA Development & Design Studio","siteUrl":"https://ui.gkhn.dev"}}