{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cta-button",
  "title": "CTA Button",
  "description": "Call-to-action button over a WebGL dither shine that sweeps at 10 fps, with a stepped pixel tilt toward the pointer on hover.",
  "dependencies": [
    "cn",
    "class-variance-authority",
    "radix-ui"
  ],
  "registryDependencies": [
    "@ui95/theme",
    "@ui95/dither-shine",
    "@ui95/use-prefers-reduced-motion"
  ],
  "files": [
    {
      "path": "components/ui/cta-button.tsx",
      "content": "\"use client\"\n\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"cn\"\nimport { Slot } from \"radix-ui\"\nimport * as React from \"react\"\n\nimport { DitherShine } from \"@/components/ui/dither-shine\"\nimport { usePrefersReducedMotion } from \"@/hooks/use-prefers-reduced-motion\"\nimport { defaultPreset, type DitherShineParams } from \"@/lib/shaders/dither-shine\"\n\nconst TILT_LEVELS = 2\n\nconst TILT_REST = 0\n\nconst ctaButtonVariants = cva(\n  \"group/cta-button relative isolate inline-flex shrink-0 items-center justify-center overflow-hidden border-solid bg-(--keycap) font-pixel text-[16px] leading-none whitespace-nowrap text-(--keycap-foreground) uppercase outline-none select-none [--tilt-x:0] [--tilt-y:0] [--tilt-lift:0] [border-top-color:var(--keycap-wall-hi)] [border-left-color:var(--keycap-wall-hi)] [border-right-color:var(--keycap-wall-lo)] [border-bottom-color:var(--keycap-wall-lo)] [border-top-width:calc((1+var(--tilt-y)*0.5)*var(--pixel))] [border-bottom-width:calc((3+var(--tilt-lift)-var(--tilt-y)*1.5)*var(--pixel))] [border-left-width:calc((2.5+var(--tilt-x)*1.5)*var(--pixel))] [border-right-width:calc((2.5-var(--tilt-x)*1.5)*var(--pixel))] [box-shadow:0_0_0_var(--pixel)_var(--keycap-outline)] focus-visible:outline-solid focus-visible:outline-offset-[calc(var(--pixel)*2)] \" +\n  \"active:[border-top-width:calc(var(--pixel)*2)] active:[border-bottom-width:calc(var(--pixel)*2.5)] active:[border-left-width:calc(var(--pixel)*2)] active:[border-right-width:calc(var(--pixel)*2.5)] active:[border-top-color:var(--keycap-wall-lo)] active:[border-left-color:var(--keycap-wall-lo)] active:[border-right-color:var(--keycap-wall-hi)] active:[border-bottom-color:var(--keycap-wall-hi)] \" +\n  \"disabled:pointer-events-none disabled:bg-muted disabled:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n  {\n    variants: {\n      size: {\n        sm: \"h-8 gap-1.5 px-2.5 [&_svg:not([class*='size-'])]:size-3.5\",\n        default: \"h-10 gap-2 px-4\",\n        lg: \"h-12 gap-2 px-6 text-[24px]\",\n      },\n    },\n    defaultVariants: {\n      size: \"default\",\n    },\n  }\n)\n\ntype CtaButtonShine = Partial<Omit<DitherShineParams, \"colorBack\" | \"colorFront\">>\n\ntype CtaButtonProps = React.ComponentProps<\"button\"> &\n  VariantProps<typeof ctaButtonVariants> & {\n    asChild?: boolean\n    isShining?: boolean\n    shine?: CtaButtonShine\n  }\n\ntype TiltTarget = {\n  element: HTMLElement | null\n  x: number\n  y: number\n  lift: number\n}\n\nfunction quantizeTilt(value: number): number {\n  return Math.round(Math.max(-1, Math.min(1, value)) * TILT_LEVELS) / TILT_LEVELS\n}\n\nfunction applyTilt(element: HTMLElement, { x, y, lift }: Omit<TiltTarget, \"element\">) {\n  element.style.setProperty(\"--tilt-x\", String(x))\n  element.style.setProperty(\"--tilt-y\", String(y))\n  element.style.setProperty(\"--tilt-lift\", String(lift))\n}\n\nfunction useSteppedTilt(fps: number) {\n  const targetRef = React.useRef<TiltTarget>({ element: null, x: TILT_REST, y: TILT_REST, lift: 0 })\n  const timerRef = React.useRef(0)\n\n  React.useEffect(() => () => window.clearInterval(timerRef.current), [])\n\n  const startTilt = React.useCallback(\n    (element: HTMLElement) => {\n      targetRef.current.element = element\n      window.clearInterval(timerRef.current)\n      timerRef.current = window.setInterval(() => {\n        const target = targetRef.current\n        if (target.element === null) {\n          return\n        }\n        applyTilt(target.element, target)\n        if (target.x === TILT_REST && target.y === TILT_REST && target.lift === 0) {\n          window.clearInterval(timerRef.current)\n        }\n      }, 1000 / fps)\n    },\n    [fps]\n  )\n\n  const aimTilt = React.useCallback((event: React.PointerEvent<HTMLElement>) => {\n    const bounds = event.currentTarget.getBoundingClientRect()\n    if (bounds.width === 0 || bounds.height === 0) {\n      return\n    }\n    targetRef.current.x = quantizeTilt(((event.clientX - bounds.left) / bounds.width) * 2 - 1)\n    targetRef.current.y = quantizeTilt(((event.clientY - bounds.top) / bounds.height) * 2 - 1)\n    targetRef.current.lift = 1\n  }, [])\n\n  const releaseTilt = React.useCallback(() => {\n    targetRef.current.x = TILT_REST\n    targetRef.current.y = TILT_REST\n    targetRef.current.lift = 0\n  }, [])\n\n  return { startTilt, aimTilt, releaseTilt }\n}\n\nfunction CtaButton({\n  className,\n  size = \"default\",\n  asChild = false,\n  isShining = true,\n  shine,\n  disabled,\n  children,\n  onPointerEnter,\n  onPointerMove,\n  onPointerLeave,\n  ...props\n}: CtaButtonProps) {\n  const Comp = asChild ? Slot.Root : \"button\"\n  const prefersReducedMotion = usePrefersReducedMotion()\n  const shouldShine = isShining && !disabled && !prefersReducedMotion\n  const { startTilt, aimTilt, releaseTilt } = useSteppedTilt(shine?.fps ?? defaultPreset.params.fps)\n\n  return (\n    <Comp\n      data-slot=\"cta-button\"\n      data-size={size}\n      disabled={disabled}\n      className={cn(ctaButtonVariants({ size }), className)}\n      onPointerEnter={(event: React.PointerEvent<HTMLButtonElement>) => {\n        onPointerEnter?.(event)\n        if (prefersReducedMotion) {\n          return\n        }\n        aimTilt(event)\n        startTilt(event.currentTarget)\n      }}\n      onPointerMove={(event: React.PointerEvent<HTMLButtonElement>) => {\n        onPointerMove?.(event)\n        if (prefersReducedMotion) {\n          return\n        }\n        aimTilt(event)\n      }}\n      onPointerLeave={(event: React.PointerEvent<HTMLButtonElement>) => {\n        onPointerLeave?.(event)\n        releaseTilt()\n      }}\n      {...props}\n    >\n      {shouldShine && (\n        <DitherShine\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute inset-0 -z-10\"\n          {...shine}\n          colorBack=\"--keycap\"\n          colorFront=\"--keycap-foreground\"\n        />\n      )}\n      <Slot.Slottable>{children}</Slot.Slottable>\n    </Comp>\n  )\n}\n\nexport { CtaButton, ctaButtonVariants }\nexport type { CtaButtonProps, CtaButtonShine }\n",
      "type": "registry:ui"
    }
  ],
  "meta": {
    "sounds": {
      "keycap.press": {
        "note": "Tactile, bigger switch: lower bump plus a little body",
        "layers": [
          {
            "wave": "noise",
            "frequency": 0,
            "attack": 0.0011,
            "decay": 0.00138,
            "gain": 0.12,
            "filter": {
              "type": "bandpass",
              "frequency": 1510,
              "q": 0.3
            },
            "curve": 4.4
          },
          {
            "wave": "square",
            "frequency": 1880,
            "attack": 0.0016,
            "decay": 0.00331,
            "gain": 0.11,
            "curve": 3.9,
            "frequencyEnd": 4690
          },
          {
            "wave": "sine",
            "frequency": 628,
            "frequencyEnd": 963,
            "pitchTime": 0.029,
            "attack": 0.0072,
            "decay": 0.00798,
            "gain": 0.05,
            "curve": 3.9,
            "delay": 0
          }
        ]
      },
      "keycap.release": {
        "note": "Tactile upstroke",
        "layers": [
          {
            "wave": "noise",
            "frequency": 0,
            "attack": 0.0003,
            "decay": 0.0033,
            "gain": 0.35,
            "filter": {
              "type": "bandpass",
              "frequency": 4780,
              "q": 2
            }
          },
          {
            "wave": "square",
            "frequency": 2120,
            "attack": 0.0005,
            "decay": 0.00233,
            "gain": 0.13,
            "frequencyEnd": 1690
          }
        ]
      }
    }
  },
  "categories": [
    "ui"
  ],
  "type": "registry:ui"
}
