{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "mask-button",
  "title": "Mask Button",
  "description": "A button whose face wipes away on hover via an animated CSS sprite-sheet mask, revealing the label behind it.",
  "registryDependencies": ["@godui/godui-theme"],
  "files": [
    {
      "path": "packages/components/src/mask-button/mask-button.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nexport type MaskButtonMask = \"nature\" | \"urban\" | \"forest\";\nexport type MaskButtonVariant = \"primary\" | \"secondary\";\nexport type MaskButtonSize = \"sm\" | \"md\" | \"lg\";\n\nexport type MaskButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {\n  /** Sprite-sheet mask animated on hover to reveal the label */\n  mask?: MaskButtonMask;\n  /** Color of the masked face */\n  variant?: MaskButtonVariant;\n  size?: MaskButtonSize;\n};\n\n// Resolve the sprite sheets through the bundler. `new URL(..., import.meta.url)`\n// returns a string URL in both Vite (Storybook) and Webpack/Turbopack (Next),\n// avoiding the StaticImageData object a bare `import` yields under Next.\nconst MASK_ASSETS: Record<MaskButtonMask, string> = {\n  nature: new URL(\"../assets/mask-nature.png\", import.meta.url).href,\n  urban: new URL(\"../assets/mask-urban.png\", import.meta.url).href,\n  forest: new URL(\"../assets/mask-forest.png\", import.meta.url).href,\n};\n\n// Per-mask sprite geometry + flipbook step counts (static so the scanner can\n// see the class names). `out` plays at rest, `in` on hover / focus-visible.\nconst maskConfig: Record<\n  MaskButtonMask,\n  { sizeClass: string; rest: string; hover: string }\n> = {\n  nature: {\n    sizeClass: \"[mask-size:2300%_100%] [-webkit-mask-size:2300%_100%]\",\n    rest: \"animate-mask-nature-out\",\n    hover:\n      \"group-hover:animate-mask-nature-in group-focus-visible:animate-mask-nature-in\",\n  },\n  urban: {\n    sizeClass: \"[mask-size:3000%_100%] [-webkit-mask-size:3000%_100%]\",\n    rest: \"animate-mask-urban-out\",\n    hover:\n      \"group-hover:animate-mask-urban-in group-focus-visible:animate-mask-urban-in\",\n  },\n  forest: {\n    sizeClass: \"[mask-size:7100%_100%] [-webkit-mask-size:7100%_100%]\",\n    rest: \"animate-mask-forest-out\",\n    hover:\n      \"group-hover:animate-mask-forest-in group-focus-visible:animate-mask-forest-in\",\n  },\n};\n\n// Reduced motion: drop the flipbook, keep a static rest→hover position swap.\nconst REDUCED_MOTION_FILL =\n  \"motion-reduce:animate-none motion-reduce:group-hover:animate-none motion-reduce:group-focus-visible:animate-none motion-reduce:[mask-position:0_0] motion-reduce:[-webkit-mask-position:0_0] motion-reduce:group-hover:[mask-position:100%_0] motion-reduce:group-hover:[-webkit-mask-position:100%_0] motion-reduce:group-focus-visible:[mask-position:100%_0] motion-reduce:group-focus-visible:[-webkit-mask-position:100%_0]\";\n\nconst fillVariant: Record<MaskButtonVariant, string> = {\n  primary: \"bg-primary text-primary-foreground\",\n  secondary: \"bg-secondary text-secondary-foreground\",\n};\n\nconst sizeClasses: Record<MaskButtonSize, string> = {\n  sm: \"px-[var(--button-px-sm)] py-[var(--button-py-sm)] text-[length:var(--button-text-sm)] leading-[var(--button-leading-sm)] rounded-[var(--button-radius-sm)]\",\n  md: \"px-[var(--button-px-md)] py-[var(--button-py-md)] text-[length:var(--button-text-md)] leading-[var(--button-leading-md)] rounded-[var(--button-radius-md)]\",\n  lg: \"px-[var(--button-px-lg)] py-[var(--button-py-lg)] text-[length:var(--button-text-lg)] leading-[var(--button-leading-lg)] rounded-[var(--button-radius-lg)]\",\n};\n\nconst BUTTON_CLASS =\n  \"group relative isolate inline-flex cursor-pointer items-center justify-center overflow-hidden border border-border bg-background font-medium text-foreground whitespace-nowrap select-none outline-none [-webkit-tap-highlight-color:transparent] [transition:scale_100ms_ease] focus-visible:[outline:2px_solid_var(--ring)] focus-visible:[outline-offset:4px] enabled:active:scale-[0.96] enabled:data-[pressed=true]:scale-[0.96] disabled:cursor-not-allowed disabled:opacity-50 disabled:pointer-events-none\";\n\nconst FILL_BASE =\n  \"absolute inset-0 z-[1] flex items-center justify-center [mask-repeat:no-repeat] [-webkit-mask-repeat:no-repeat] [mask-image:var(--mask-img)] [-webkit-mask-image:var(--mask-img)]\";\n\nconst MaskButton = React.forwardRef<HTMLButtonElement, MaskButtonProps>(\n  (\n    {\n      className,\n      children,\n      mask = \"nature\",\n      variant = \"primary\",\n      size = \"md\",\n      onKeyDown,\n      onKeyUp,\n      ...props\n    },\n    ref,\n  ) => {\n    const [pressed, setPressed] = React.useState(false);\n\n    const handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {\n      if (event.key === \"Enter\" || event.key === \" \") {\n        setPressed(true);\n      }\n      onKeyDown?.(event);\n    };\n\n    const handleKeyUp = (event: React.KeyboardEvent<HTMLButtonElement>) => {\n      if (event.key === \"Enter\" || event.key === \" \") {\n        setPressed(false);\n      }\n      onKeyUp?.(event);\n    };\n\n    const cfg = maskConfig[mask];\n\n    return (\n      <button\n        ref={ref}\n        type=\"button\"\n        data-mask={mask}\n        data-variant={variant}\n        data-size={size}\n        data-pressed={pressed ? \"true\" : undefined}\n        onKeyDown={handleKeyDown}\n        onKeyUp={handleKeyUp}\n        className={`${BUTTON_CLASS} ${sizeClasses[size]} ${className ?? \"\"}`}\n        {...props}\n      >\n        <span className=\"relative z-0\">{children}</span>\n        <span\n          className={`${FILL_BASE} ${fillVariant[variant]} ${cfg.sizeClass} ${cfg.rest} ${cfg.hover} ${REDUCED_MOTION_FILL}`}\n          style={\n            {\n              \"--mask-img\": `url(\"${MASK_ASSETS[mask]}\")`,\n            } as React.CSSProperties\n          }\n          aria-hidden=\"true\"\n        >\n          {children}\n        </span>\n      </button>\n    );\n  },\n);\nMaskButton.displayName = \"MaskButton\";\n\nexport { MaskButton };\n",
      "type": "registry:ui",
      "target": "components/godui/mask-button.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "animate-mask-nature-in": "mask-button-in 0.7s steps(22) forwards",
      "animate-mask-nature-out": "mask-button-out 0.7s steps(22) forwards",
      "animate-mask-urban-in": "mask-button-in 0.7s steps(29) forwards",
      "animate-mask-urban-out": "mask-button-out 0.7s steps(29) forwards",
      "animate-mask-forest-in": "mask-button-in 0.7s steps(70) forwards",
      "animate-mask-forest-out": "mask-button-out 0.7s steps(70) forwards"
    }
  },
  "css": {
    "@keyframes mask-button-in": {
      "from": {
        "-webkit-mask-position": "0 0",
        "mask-position": "0 0"
      },
      "to": {
        "-webkit-mask-position": "100% 0",
        "mask-position": "100% 0"
      }
    },
    "@keyframes mask-button-out": {
      "from": {
        "-webkit-mask-position": "100% 0",
        "mask-position": "100% 0"
      },
      "to": {
        "-webkit-mask-position": "0 0",
        "mask-position": "0 0"
      }
    }
  },
  "type": "registry:ui"
}
