{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "a11y",
  "title": "A11y",
  "description": "SkipLink, VisuallyHidden, LiveRegion/announce() and useReducedMotion.",
  "dependencies": [
    "@radix-ui/react-slot"
  ],
  "registryDependencies": [
    "@hjconsult/lib",
    "@hjconsult/motion"
  ],
  "files": [
    {
      "path": "src/a11y/index.ts",
      "content": "export { useReducedMotion } from '../motion/use-reduced-motion'\nexport {\n  announce,\n  LIVE_REGION_ID,\n  LiveRegion,\n  type LiveRegionProps,\n  type Politeness,\n} from './live-region'\nexport { SkipLink, type SkipLinkProps } from './skip-link'\nexport { VisuallyHidden, type VisuallyHiddenProps } from './visually-hidden'\n",
      "type": "registry:lib",
      "target": "hj/a11y/index.ts"
    },
    {
      "path": "src/a11y/visually-hidden.tsx",
      "content": "import { Slot } from '@radix-ui/react-slot'\nimport type { ComponentPropsWithoutRef, Ref } from 'react'\nimport { cn } from '../lib'\n\nexport interface VisuallyHiddenProps extends ComponentPropsWithoutRef<'span'> {\n  /** Render the child element instead of a `span`, merging the hidden styles onto it. */\n  asChild?: boolean\n  ref?: Ref<HTMLSpanElement>\n}\n\n/** Content for assistive technology only. Stays in the accessibility tree, leaves the layout. */\nexport function VisuallyHidden({ asChild = false, className, ...props }: VisuallyHiddenProps) {\n  const Comp = asChild ? Slot : 'span'\n  return <Comp className={cn('sr-only', className)} {...props} />\n}\n",
      "type": "registry:lib",
      "target": "hj/a11y/visually-hidden.tsx"
    },
    {
      "path": "src/a11y/skip-link.tsx",
      "content": "import type { ComponentPropsWithoutRef, Ref } from 'react'\nimport { cn, focusRing } from '../lib'\n\nexport interface SkipLinkProps extends Omit<ComponentPropsWithoutRef<'a'>, 'href'> {\n  /** `id` of the element to jump to (without `#`). */\n  target: string\n  ref?: Ref<HTMLAnchorElement>\n}\n\n/**\n * Keyboard users' first tab stop. Visually hidden until focused, then pinned to the viewport.\n * Place it as the first child of `<body>` and give `<main>` the matching `id`.\n */\nexport function SkipLink({ target, className, children, ...props }: SkipLinkProps) {\n  return (\n    <a\n      href={`#${target}`}\n      className={cn(\n        'sr-only focus-visible:not-sr-only focus-visible:fixed focus-visible:top-4 focus-visible:left-4 focus-visible:z-50',\n        'focus-visible:inline-flex focus-visible:items-center focus-visible:rounded-pill focus-visible:bg-accent focus-visible:px-5 focus-visible:py-3',\n        'font-body text-small font-medium text-fg-on-accent shadow-3',\n        focusRing,\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </a>\n  )\n}\n",
      "type": "registry:lib",
      "target": "hj/a11y/skip-link.tsx"
    },
    {
      "path": "src/a11y/live-region.tsx",
      "content": "import type { ComponentPropsWithoutRef, Ref } from 'react'\nimport { cn } from '../lib'\n\nexport type Politeness = 'polite' | 'assertive'\n\nexport const LIVE_REGION_ID = 'hj-live-region'\n\nexport interface LiveRegionProps extends ComponentPropsWithoutRef<'div'> {\n  politeness?: Politeness\n  ref?: Ref<HTMLDivElement>\n}\n\n/**\n * A persistent, visually hidden live region. Mount one near the root of the app so\n * `announce()` can reuse it instead of creating nodes on the fly.\n */\nexport function LiveRegion({ politeness = 'polite', className, ...props }: LiveRegionProps) {\n  return (\n    <div\n      id={politeness === 'polite' ? LIVE_REGION_ID : `${LIVE_REGION_ID}-assertive`}\n      role={politeness === 'assertive' ? 'alert' : 'status'}\n      aria-live={politeness}\n      aria-atomic=\"true\"\n      className={cn('sr-only', className)}\n      {...props}\n    />\n  )\n}\n\nfunction getRegion(politeness: Politeness): HTMLElement | null {\n  if (typeof document === 'undefined') return null\n  const id = politeness === 'polite' ? LIVE_REGION_ID : `${LIVE_REGION_ID}-assertive`\n  const existing = document.getElementById(id)\n  if (existing) return existing\n  const node = document.createElement('div')\n  node.id = id\n  node.setAttribute('role', politeness === 'assertive' ? 'alert' : 'status')\n  node.setAttribute('aria-live', politeness)\n  node.setAttribute('aria-atomic', 'true')\n  node.className = 'sr-only'\n  document.body.appendChild(node)\n  return node\n}\n\n/**\n * Announce a message to screen readers. The region is cleared first so repeated identical\n * messages are still read. Safe to call on the server (no-op).\n */\nexport function announce(message: string, politeness: Politeness = 'polite'): void {\n  const region = getRegion(politeness)\n  if (!region) return\n  region.textContent = ''\n  // Let the DOM settle so assistive technology sees a fresh mutation.\n  window.setTimeout(() => {\n    region.textContent = message\n  }, 0)\n}\n",
      "type": "registry:lib",
      "target": "hj/a11y/live-region.tsx"
    }
  ],
  "cssVars": {},
  "type": "registry:lib"
}