{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "focus-ring",
  "type": "registry:ui",
  "title": "Focus Ring",
  "description": "A `<span>` that paints the DS focus ring when anything inside it takes focus — `focus-within` on the wrapper, not `focus-visible` on the child.",
  "author": "ofri-peretz <https://github.com/ofri-peretz>",
  "categories": [
    "a11y",
    "primitive"
  ],
  "dependencies": [],
  "registryDependencies": [
    "https://ds.interlace.tools/r/theme.json",
    "https://ds.interlace.tools/r/cn.json"
  ],
  "files": [
    {
      "path": "registry/interlace-ui/focus-ring.tsx",
      "target": "components/ui/focus-ring.tsx",
      "type": "registry:ui",
      "content": "import * as React from 'react';\n\n// @interlace/focus-ring v1.2.0 — Interlace design system.\n// Docs, props and live preview: https://ds.interlace.tools/c/focus-ring\n// What changed since: https://ds.interlace.tools/c/focus-ring#history\n// Generated banner — keep it, the upgrade diff reads this version.\n\n/**\n * @interlace/ui — FocusRing\n *\n * A `<span>` that paints the DS focus ring when anything inside it takes\n * focus — `focus-within` on the wrapper, not `focus-visible` on the child.\n *\n * Reach for it when composing a custom interactive surface (a card-as-button,\n * a clickable row), or in a subtree that opted out of the global ring.\n *\n * Two consequences of putting the ring on a wrapper. It works even when the\n * child sets its own outline, which is the point. And it is `focus-within`,\n * not `focus-visible`: a MOUSE click that lands focus inside will paint the\n * ring too, where the global preflight contract would not have.\n *\n * It renders one real DOM node per wrapped surface — a `<span>`, always. There\n * is no `as` prop and the span is not optional; measure before wrapping\n * thousands of rows.\n *\n * ## Anatomy\n *\n *   FocusRing                         (span — data-min-viewport=320)\n *     └─ children                     (the focusable element)\n *\n * ## MIN_VIEWPORT — 320\n *\n * The focus contract is universal. No device too small to honor it.\n *\n * | Rule | Concept                          | Where in this file                                          |\n * | ---- | -------------------------------- | ----------------------------------------------------------- |\n * | R4   | Extends native el                | `React.ComponentProps<'span'> & FocusRingProps`             |\n * | R6   | data-slot on root                | `data-slot=\"focus-ring\"`                                    |\n * | R7   | className merged + ...rest       | `cn(BASE, className)` + `{...props}`                        |\n * | R8   | Enum for offset                  | `offset = 'none' | 'sm' | 'md' | 'lg'`                      |\n * | R10  | Composition seam                 | `className` (the wrapper's display is the caller's call)    |\n * | R14  | Declares min viewport            | `data-min-viewport={String(MIN_VIEWPORT)}` + exported const |\n * | R18  | Tailwind only                    | Zero inline `style`; utility classes only                   |\n * | R19  | Tokens only                      | `--ring` semantic token                                     |\n * | R20  | AA contrast                      | `--ring` is contrast-tuned by interlace-theme.css per mode  |\n * | R25  | Server component                 | No hooks → no `'use client'`                                |\n * | R26  | A11y per WCAG 2.2 SC 2.4.13      | 2px solid ring, ≥3:1 contrast, configurable offset          |\n */\n\nimport { cn } from '@/lib/utils';\n\nexport const MIN_VIEWPORT = 320 as const;\n\ntype FocusRingOffset = 'none' | 'sm' | 'md' | 'lg';\n\nconst OFFSET: Record<FocusRingOffset, string> = {\n  none: 'focus-within:ring-offset-0',\n  sm: 'focus-within:ring-offset-1',\n  md: 'focus-within:ring-offset-2',\n  lg: 'focus-within:ring-offset-4',\n};\n\ntype FocusRingProps = React.ComponentProps<'span'> & {\n  /**\n   * Distance between the focused element and the ring. Defaults to `md`\n   * (2 px), matching the preflight contract.\n   */\n  offset?: FocusRingOffset;\n};\n\n/**\n * Renders a `<span>`, `inline-block` by default.\n *\n * **Wrapping a block-level child? Pass `className=\"block\"`.** This is not a\n * style preference — an `inline-block` box in normal flow whose child is a\n * `display: block` element with `width: auto` is a circular width dependency,\n * and Chrome resolves it to **zero**: the wrapper measures 0px, the child\n * overflows it, and the content renders one word per line. That shipped as\n * this component's live preview on the public registry.\n *\n * There is deliberately no `as` prop. The contract table used to claim one and\n * none was ever implemented, so `as=\"div\"` landed in `...props` and was written\n * onto the span as an invalid DOM attribute. Per CONVENTIONS.md the DS\n * composition seam is Base UI's `render` prop, not `as`.\n */\nexport const FocusRing = React.forwardRef<HTMLSpanElement, FocusRingProps>(\n  ({ className, offset = 'md', children, ...props }, ref) => (\n    <span\n      ref={ref}\n      data-slot=\"focus-ring\"\n      data-min-viewport={String(MIN_VIEWPORT)}\n      className={cn(\n        'inline-block rounded-md',\n        'focus-within:outline-none focus-within:ring-2 focus-within:ring-ring',\n        OFFSET[offset],\n        'transition-shadow',\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </span>\n  ),\n);\nFocusRing.displayName = 'FocusRing';\n"
    }
  ],
  "meta": {
    "tier": "primitive",
    "client": false,
    "minViewport": 320,
    "loading": false,
    "version": "1.2.0",
    "since": "1.0.0"
  },
  "docs": "## @interlace/focus-ring\n\nInstalled to `components/ui/focus-ring.tsx`.\n\n```tsx\nimport { /* … */ } from '@/components/ui/focus-ring';\n```\n\nProps, a11y contract, live preview and source: https://ds.interlace.tools/c/focus-ring\n\nRequires the `@interlace/theme` CSS baseline (installed automatically as a registry dependency)."
}
