{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"star-border","type":"registry:component","title":"Star Border","description":"A slow light orbits the border of a CTA button or card — a pure-CSS conic-gradient wrapper with color, speed and thickness props. Reduced-motion freezes it into a static ring.","author":"UI Lab","dependencies":["clsx","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/star-border.tsx","type":"registry:component","target":"@components/motion/star-border.tsx","content":"// ui-lab-ten.vercel.app/components/motion/star-border\n// Ported from motion-anything (nexu-io, Apache-2.0); upstream: reactbits.dev \"Star Border\", redistributed with permission.\n// The two-layer comet head/tail, the mask-cut ring and the animated-angle\n// approach are a clean-room reimplementation of techniques observed on\n// unabyss.com; no upstream source was used.\n\nimport type { CSSProperties, ReactNode } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface StarBorderProps {\n  /** Content the light ring wraps. Needs no background of its own — the ring is cut to the border band. */\n  children: ReactNode;\n  className?: string;\n  /** Color of the traveling light. Defaults to the current text color, so it follows whatever `text-*` class is set on the wrapper. */\n  color?: string;\n  /** Seconds for one full revolution around the border. */\n  speed?: number;\n  /** Border ring thickness in px. */\n  thickness?: number;\n  /**\n   * Where the comet sits at time zero, in degrees. Vary it across instances so\n   * several rings on one screen don't travel in lockstep.\n   */\n  startAngle?: number;\n}\n\n/**\n * Registering the angle as a custom property is what makes it animatable: the\n * browser can then interpolate `--uilab-comet-angle` and only the gradient is\n * repainted. The previous approach spun a huge element instead, which meant\n * compositing a layer many times the size of the button.\n *\n * Where `@property` is unsupported the angle simply never advances, leaving the\n * static ring — the same thing reduced motion asks for.\n */\nconst COMET_STYLES = `\n@property --uilab-comet-angle{syntax:\"<angle>\";inherits:false;initial-value:0deg}\n@keyframes uilab-comet-spin{to{--uilab-comet-angle:360deg}}\n@media (prefers-reduced-motion:reduce){.uilab-comet-layer{animation:none!important}}\n`;\n\n/** Cuts a painted box down to just its padding band, leaving the middle clear. */\nconst RING_MASK =\n  \"linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0)\";\n\nfunction mix(color: string, percent: number): string {\n  return `color-mix(in srgb, ${color} ${percent}%, transparent)`;\n}\n\n/**\n * A comet is a bright head with a long faint trail, so it takes two passes: a\n * wide soft tail and a narrow bright core, both anchored to the same angle.\n * One arc alone reads as a rotating smear.\n */\nfunction cometGradient(color: string, layer: \"tail\" | \"core\"): string {\n  const from = \"calc(var(--uilab-comet-angle) + var(--uilab-comet-start))\";\n\n  // Stops run outward from 0deg and wrap back through 360deg, so the trail\n  // follows the head rather than leading it.\n  const stops =\n    layer === \"tail\"\n      ? `${mix(color, 30)} 0deg, ${mix(color, 14)} 12deg, ${mix(color, 4)} 30deg, transparent 60deg, transparent 240deg, ${mix(color, 4)} 270deg, ${mix(color, 14)} 320deg, ${mix(color, 30)} 360deg`\n      : `${mix(color, 85)} 0deg, ${mix(color, 40)} 5deg, ${mix(color, 8)} 14deg, transparent 26deg, transparent 334deg, ${mix(color, 8)} 346deg, ${mix(color, 40)} 355deg, ${mix(color, 85)} 360deg`;\n\n  return `conic-gradient(from ${from}, ${stops})`;\n}\n\n/**\n * A slow beam of light travels around an element's border — a quiet ambient\n * accent for a single CTA or \"featured\" card.\n *\n * Pure CSS, no dependency: two conic-gradient layers (comet tail + head) are\n * masked down to the border band, so the ring works over any background,\n * including a transparent or blurred card. Reduced motion freezes the comet in\n * place rather than hiding it.\n */\nexport function StarBorder({\n  children,\n  className,\n  color = \"currentColor\",\n  speed = 6,\n  thickness = 1,\n  startAngle = 0,\n}: StarBorderProps) {\n  const layer: CSSProperties = {\n    padding: thickness,\n    animationDuration: `${speed}s`,\n    mask: RING_MASK,\n    WebkitMask: RING_MASK,\n    backgroundOrigin: \"border-box\",\n    backgroundClip: \"border-box\",\n  };\n\n  return (\n    <>\n      <style>{COMET_STYLES}</style>\n      <div\n        className={cn(\"relative isolate inline-block rounded-2xl\", className)}\n        style={\n          {\n            padding: thickness,\n            \"--uilab-comet-start\": `${startAngle}deg`,\n          } as CSSProperties\n        }\n      >\n        <span\n          aria-hidden\n          className=\"uilab-comet-layer pointer-events-none absolute inset-0 rounded-[inherit] animate-[uilab-comet-spin_6s_linear_infinite]\"\n          style={{ ...layer, background: cometGradient(color, \"tail\") }}\n        />\n        <span\n          aria-hidden\n          className=\"uilab-comet-layer pointer-events-none absolute inset-0 rounded-[inherit] animate-[uilab-comet-spin_6s_linear_infinite]\"\n          style={{ ...layer, background: cometGradient(color, \"core\") }}\n        />\n        <div className=\"relative z-[2]\">{children}</div>\n      </div>\n    </>\n  );\n}\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@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"}]}