{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pixel-heading-word",
  "type": "registry:ui",
  "description": "Whole-word pixel-font heading that swaps or cycles fonts on hover using Geist pixel fonts",
  "dependencies": [
    "geist"
  ],
  "files": [
    {
      "path": "registry/default/ui/pixel-heading-word.tsx",
      "content": "/**\n * @module PixelHeading\n *\n * Setup — Geist Pixel Fonts with Tailwind CSS\n * =============================================\n *\n * All Geist fonts can be used through CSS variables:\n *\n *   GeistSans:          --font-geist-sans\n *   GeistMono:          --font-geist-mono\n *   GeistPixelSquare:   --font-geist-pixel-square\n *   GeistPixelGrid:     --font-geist-pixel-grid\n *   GeistPixelCircle:   --font-geist-pixel-circle\n *   GeistPixelTriangle: --font-geist-pixel-triangle\n *   GeistPixelLine:     --font-geist-pixel-line\n *\n * 1. Register the font variables in app/layout.js:\n *\n *   ```js\n *   import { GeistSans } from \"geist/font/sans\";\n *   import { GeistMono } from \"geist/font/mono\";\n *   import { GeistPixelSquare } from \"geist/font/pixel\";\n *\n *   export default function RootLayout({ children }) {\n *     return (\n *       <html\n *         lang=\"en\"\n *         className={`${GeistSans.variable} ${GeistMono.variable} ${GeistPixelSquare.variable}`}\n *       >\n *         <body>{children}</body>\n *       </html>\n *     );\n *   }\n *   ```\n *\n * 2. Map the CSS variables in your Tailwind CSS v4 theme (tailwind.css):\n *\n *   ```css\n *   @theme {\n *     --font-sans: var(--font-geist-sans);\n *     --font-mono: var(--font-geist-mono);\n *     --font-pixel-square: var(--font-geist-pixel-square);\n *     --font-pixel-grid: var(--font-geist-pixel-grid);\n *     --font-pixel-circle: var(--font-geist-pixel-circle);\n *     --font-pixel-triangle: var(--font-geist-pixel-triangle);\n *     --font-pixel-line: var(--font-geist-pixel-line);\n *   }\n *   ```\n *\n * Once configured, the `font-pixel-*` utility classes used by this\n * component will resolve correctly.\n */\n\n\"use client\"\n\nimport { useCallback, useEffect, useRef, useState } from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype PixelFont = \"square\" | \"grid\" | \"circle\" | \"triangle\" | \"line\"\n\nconst PIXEL_FONT_MAP: Record<PixelFont, string> = {\n  square: \"font-pixel-square\",\n  grid: \"font-pixel-grid\",\n  circle: \"font-pixel-circle\",\n  triangle: \"font-pixel-triangle\",\n  line: \"font-pixel-line\",\n}\n\nconst PIXEL_FONTS = Object.values(PIXEL_FONT_MAP)\nconst PIXEL_FONT_KEYS = Object.keys(PIXEL_FONT_MAP) as PixelFont[]\n\n/**\n * Props for the PixelHeading component.\n *\n * Extends native heading element attributes so all standard HTML\n * props (id, aria-*, data-*, event handlers) are forwarded.\n */\nexport interface PixelHeadingProps extends React.ComponentProps<\"h1\"> {\n  /**\n   * The heading level to render.\n   * @default \"h1\"\n   */\n  as?: \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\"\n  /**\n   * The resting pixel font displayed by default.\n   * @default \"square\"\n   */\n  initialFont?: PixelFont\n  /**\n   * The pixel font to show on hover / focus.\n   * When set the component swaps between `initialFont` and `hoverFont`\n   * instead of cycling through every font.\n   */\n  hoverFont?: PixelFont\n  /**\n   * Interval in milliseconds between font cycles on hover.\n   * Only used when `hoverFont` is **not** set (cycling mode).\n   * @default 300\n   */\n  cycleInterval?: number\n  /**\n   * Initial font index (0–4) for the starting pixel font.\n   * Ignored when `initialFont` is set.\n   * @default 0\n   */\n  defaultFontIndex?: number\n  /**\n   * Callback fired when the active font index changes.\n   */\n  onFontIndexChange?: (index: number) => void\n  /**\n   * Whether to show the font name label beneath the heading.\n   * @default true\n   */\n  showLabel?: boolean\n  /**\n   * Disable all hover / focus interactions.\n   * The heading stays locked to `initialFont` (or `defaultFontIndex`).\n   * @default false\n   */\n  disableHover?: boolean\n  /**\n   * Disable the auto-cycling interval in cycle mode.\n   * Swap mode (`hoverFont`) still works unless `disableHover` is also set.\n   * @default false\n   */\n  disableCycling?: boolean\n}\n\n/**\n * Interactive heading that swaps or cycles through pixel font styles on hover.\n *\n * **Swap mode** — set `initialFont` and `hoverFont` to swap between two\n * specific fonts on hover:\n *\n * @example\n * <PixelHeading initialFont=\"square\" hoverFont=\"circle\" className=\"text-6xl\">\n *   Swap on hover\n * </PixelHeading>\n *\n * **Cycle mode** — omit `hoverFont` to cycle through every pixel font on\n * hover (the original behavior):\n *\n * @example\n * <PixelHeading\n *   as=\"h2\"\n *   cycleInterval={200}\n *   onFontIndexChange={(i) => console.log(i)}\n * >\n *   Cycle on hover\n * </PixelHeading>\n */\nexport function PixelHeading({\n  children,\n  as: Tag = \"h1\",\n  className,\n  initialFont,\n  hoverFont,\n  cycleInterval = 300,\n  defaultFontIndex = 0,\n  onFontIndexChange,\n  showLabel = false,\n  disableHover = false,\n  disableCycling = false,\n  onMouseEnter,\n  onMouseLeave,\n  onFocus,\n  onBlur,\n  onKeyDown,\n  ...props\n}: PixelHeadingProps) {\n  /* ------------------------------------------------------------------ */\n  /* Resolve the starting index from `initialFont` or `defaultFontIndex` */\n  /* ------------------------------------------------------------------ */\n  const resolvedDefaultIndex = initialFont\n    ? PIXEL_FONT_KEYS.indexOf(initialFont)\n    : defaultFontIndex\n\n  const hoverIndex = hoverFont ? PIXEL_FONT_KEYS.indexOf(hoverFont) : null\n  const isSwapMode = hoverIndex !== null\n\n  const [fontIndex, setFontIndex] = useState(resolvedDefaultIndex)\n  const [isActive, setIsActive] = useState(false)\n  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)\n\n  useEffect(() => {\n    return () => {\n      if (intervalRef.current) {\n        clearInterval(intervalRef.current)\n      }\n    }\n  }, [])\n\n  /* ---- Cycling helpers (cycle mode only) ---- */\n  const advanceFont = useCallback(() => {\n    setFontIndex((prev) => {\n      const next = (prev + 1) % PIXEL_FONTS.length\n      onFontIndexChange?.(next)\n      return next\n    })\n  }, [onFontIndexChange])\n\n  const startCycling = useCallback(() => {\n    setIsActive(true)\n    intervalRef.current = setInterval(advanceFont, cycleInterval)\n  }, [advanceFont, cycleInterval])\n\n  const stopCycling = useCallback(() => {\n    setIsActive(false)\n    if (intervalRef.current) {\n      clearInterval(intervalRef.current)\n      intervalRef.current = null\n    }\n  }, [])\n\n  /* ---- Swap helpers ---- */\n  const swapToHover = useCallback(() => {\n    if (hoverIndex === null) return\n    setIsActive(true)\n    setFontIndex(hoverIndex)\n    onFontIndexChange?.(hoverIndex)\n  }, [hoverIndex, onFontIndexChange])\n\n  const swapToInitial = useCallback(() => {\n    setIsActive(false)\n    setFontIndex(resolvedDefaultIndex)\n    onFontIndexChange?.(resolvedDefaultIndex)\n  }, [resolvedDefaultIndex, onFontIndexChange])\n\n  /* ---- Event handlers ---- */\n  const handleMouseEnter = useCallback(\n    (e: React.MouseEvent<HTMLHeadingElement>) => {\n      if (!disableHover) {\n        if (isSwapMode) {\n          swapToHover()\n        } else if (!disableCycling) {\n          startCycling()\n        }\n      }\n      onMouseEnter?.(e)\n    },\n    [\n      disableHover,\n      disableCycling,\n      isSwapMode,\n      swapToHover,\n      startCycling,\n      onMouseEnter,\n    ]\n  )\n\n  const handleMouseLeave = useCallback(\n    (e: React.MouseEvent<HTMLHeadingElement>) => {\n      if (!disableHover) {\n        isSwapMode ? swapToInitial() : stopCycling()\n      }\n      onMouseLeave?.(e)\n    },\n    [disableHover, isSwapMode, swapToInitial, stopCycling, onMouseLeave]\n  )\n\n  const handleFocus = useCallback(\n    (e: React.FocusEvent<HTMLHeadingElement>) => {\n      if (!disableHover) {\n        isSwapMode ? swapToHover() : setIsActive(true)\n      }\n      onFocus?.(e)\n    },\n    [disableHover, isSwapMode, swapToHover, onFocus]\n  )\n\n  const handleBlur = useCallback(\n    (e: React.FocusEvent<HTMLHeadingElement>) => {\n      if (!disableHover) {\n        isSwapMode ? swapToInitial() : setIsActive(false)\n      }\n      onBlur?.(e)\n    },\n    [disableHover, isSwapMode, swapToInitial, onBlur]\n  )\n\n  const handleKeyDown = useCallback(\n    (e: React.KeyboardEvent<HTMLHeadingElement>) => {\n      if (!disableHover && !disableCycling) {\n        if (e.key === \"Enter\" || e.key === \" \") {\n          e.preventDefault()\n          if (!isSwapMode) advanceFont()\n        }\n      }\n      onKeyDown?.(e)\n    },\n    [disableHover, disableCycling, isSwapMode, advanceFont, onKeyDown]\n  )\n\n  const currentFontLabel = PIXEL_FONT_KEYS[fontIndex]\n\n  return (\n    <div\n      data-slot=\"pixel-heading\"\n      className=\"inline-flex flex-col items-start gap-2\"\n    >\n      <Tag\n        data-state={isActive ? \"active\" : \"idle\"}\n        data-font={currentFontLabel}\n        tabIndex={0}\n        className={cn(\n          \"cursor-default transition-all duration-150 select-none\",\n          \"focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none\",\n          PIXEL_FONTS[fontIndex],\n          className\n        )}\n        onMouseEnter={handleMouseEnter}\n        onMouseLeave={handleMouseLeave}\n        onFocus={handleFocus}\n        onBlur={handleBlur}\n        onKeyDown={handleKeyDown}\n        {...props}\n      >\n        {children}\n      </Tag>\n      {showLabel && (\n        <output\n          data-slot=\"pixel-heading-label\"\n          aria-live=\"polite\"\n          className={cn(\n            \"text-muted-foreground text-xs tracking-widest uppercase transition-opacity duration-200\",\n            isActive ? \"opacity-100\" : \"opacity-0\"\n          )}\n        >\n          {currentFontLabel}\n        </output>\n      )}\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ]
}