{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "avatar",
  "title": "Avatar",
  "description": "A person is a ring and their initial, in italic, since a name is theirs; or a monogram of both initials; or the given name set to fill one width.",
  "registryDependencies": [
    "https://thedirectors.agency/ui/r/0nlytype.json"
  ],
  "files": [
    {
      "path": "registry/0nlytype/ui/avatar.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { useComposedRefs } from \"@/lib/0nlytype/refs\"\nimport { cn } from \"@/lib/utils\"\n\ntype AvatarProps = Omit<React.ComponentProps<\"span\">, \"children\"> & {\n  /** Who this is. It names the avatar, so say the person's name. */\n  alt: string\n  src?: string\n  /** What shows without an image. Defaults to the first letter of alt (the given name, for fit), in italic. */\n  fallback?: string\n  size?: \"s\" | \"m\" | \"l\"\n  /**\n   * ring: a ring and the initial. monogram: the given initial in italic printed over the family\n   * initial in a heavy narrow roman, as the poster's \"has\" crosses \"It\". fit: the given name alone,\n   * set to fill one width, so a short name stands large and a long one small.\n   */\n  variant?: \"ring\" | \"monogram\" | \"fit\"\n  /** This person is here now: the accent dot; in a monogram the italic takes the accent; fit hangs an accent full stop. */\n  here?: boolean\n  /** The overflow count at the end of a group (\"+3\"): set in the voice, not the italic. */\n  count?: boolean\n}\n\nconst words = (name: string) => name.trim().split(/\\s+/)\n\n/** Fit: measure the name at 100px in its own face and set the size that fills the box. Re-measures when the pair changes. */\nfunction useFit(ref: React.RefObject<HTMLSpanElement | null>, text: string, on: boolean) {\n  React.useLayoutEffect(() => {\n    const el = ref.current\n    if (!on || !el) return\n    let live = true\n    const ctx = document.createElement(\"canvas\").getContext(\"2d\")\n    const fit = async () => {\n      if (!live || !ctx) return\n      const cs = getComputedStyle(el)\n      const font = `${cs.fontStyle} ${cs.fontWeight} 100px ${cs.fontFamily}`\n      try {\n        await document.fonts.load(font, text)\n      } catch {\n        return // keep the fallback size if the face cannot load\n      }\n      if (!live) return\n      ctx.font = font\n      const w = ctx.measureText(text).width\n      if (w) el.style.setProperty(\"--fit\", `${(100 * el.clientWidth) / w}px`)\n    }\n    fit()\n    // ponytail: the box is set in rem, so only the pair (on <html>) changes the fit; add a ResizeObserver if the box ever follows its container.\n    const mo = new MutationObserver(fit)\n    mo.observe(document.documentElement, { attributes: true, attributeFilter: [\"data-pair\", \"data-scheme\", \"data-mode\", \"data-key\"] })\n    return () => { live = false; mo.disconnect() }\n  }, [ref, text, on])\n}\n\n/** A person is a ring and their initial, in italic, since a name is theirs. */\nfunction Avatar({ alt, src, fallback, size = \"m\", variant = \"ring\", here, count, className, ref, ...props }: AvatarProps) {\n  const own = React.useRef<HTMLSpanElement>(null)\n  const [given, ...rest] = words(alt)\n  const fitted = variant === \"fit\" && !src && !count\n  const label = fallback ?? (variant === \"fit\" ? given : alt.charAt(0).toUpperCase())\n  useFit(own, label, fitted)\n\n  let face: React.ReactNode = label\n  if (src) {\n    // eslint-disable-next-line @next/next/no-img-element -- a registry item is framework-agnostic\n    face = <img src={src} alt=\"\" />\n  } else if (variant === \"monogram\" && !count) {\n    const family = rest.at(-1)\n    face = (\n      <>\n        <span className=\"ot-avatar-given\">{(fallback ?? given).charAt(0).toUpperCase()}</span>\n        {family && <span className=\"ot-avatar-family\">{family.charAt(0).toUpperCase()}</span>}\n      </>\n    )\n  }\n\n  const composedRef = useComposedRefs(own, ref)\n  return (\n    <span\n      ref={composedRef}\n      data-slot=\"avatar\"\n      role=\"img\"\n      aria-label={here ? `${alt}, here now` : alt}\n      data-size={size === \"m\" ? undefined : size}\n      data-variant={variant === \"ring\" ? undefined : variant}\n      dir={variant === \"ring\" ? undefined : \"auto\"} // the letters follow the name's own script, not the page's\n      data-here={here || undefined}\n      data-count={count || undefined}\n      className={cn(\"ot-avatar\", className)}\n      {...props}\n    >\n      {face}\n    </span>\n  )\n}\n\n/** Avatars overlap, and step apart when pointed at; monograms stand side by side, and fitted names stack between hairlines. Its label says who the group is. */\nfunction AvatarGroup({ className, ...props }: React.ComponentProps<\"span\">) {\n  return <span data-slot=\"avatar-group\" role=\"group\" className={cn(\"ot-avatars\", className)} {...props} />\n}\n\nexport { Avatar, AvatarGroup, type AvatarProps }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/0nlytype/styles/avatar.css",
      "content": "@layer components {\n  /* ── ot-avatar ── a person: a ring and their initial, in italic */\n  .ot-avatar {\n    --s: 2.75rem; position: relative; flex: none; display: inline-grid; place-items: center; width: var(--s); height: var(--s); border-radius: 50%;\n    background: var(--ot-paper); box-shadow: inset 0 0 0 var(--ot-hairline) var(--ot-rule-strong);\n    font-family: var(--ot-expression); font-style: italic; font-weight: 400; font-size: calc(var(--s) * 0.46); line-height: 1; color: var(--ot-ink);\n  }\n  .ot-avatar[data-size=\"s\"] { --s: 1.75rem; }\n  .ot-avatar[data-size=\"l\"] { --s: 5rem; }\n  .ot-avatar > img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; filter: grayscale(1); }\n  .ot-avatar[data-here]::after { content: \"\"; position: absolute; inset-inline-end: 4%; bottom: 4%; width: max(7px, calc(var(--s) * 0.14)); aspect-ratio: 1; border-radius: 50%; background: var(--ot-accent); box-shadow: 0 0 0 2px var(--ot-paper); }\n  .ot-avatar[data-count] { font-family: var(--ot-voice); font-style: normal; font-size: calc(var(--s) * 0.3); color: var(--ot-graphite); direction: ltr; unicode-bidi: isolate; }\n  .ot-avatars { display: inline-flex; }\n  .ot-avatars > .ot-avatar + .ot-avatar { margin-inline-start: calc(var(--s) * -0.28); transition: margin var(--ot-moderato) var(--ot-spiccato); }\n  .ot-avatars:is(:hover, [data-force~=\"hover\"]) > .ot-avatar + .ot-avatar { margin-inline-start: var(--ot-space-1); }\n\n  /* ── monogram ── \"It has to be design.\": the given initial in italic printed over the family initial in a heavy narrow roman,\n     cut out of it by a paper outline. Here now, the italic takes the accent, as \"design.\" does. Pointed at, the italic steps off\n     the roman so both letters read. */\n  .ot-avatar:is([data-variant=\"monogram\"], [data-variant=\"fit\"]) { border-radius: 0; background: none; box-shadow: none; }\n  .ot-avatar[data-variant=\"monogram\"]:not([data-count]) { display: inline-flex; align-items: baseline; justify-content: center; width: auto; min-width: var(--s); font-size: calc(var(--s) * 0.62); line-height: 1; }\n  .ot-avatar-given {\n    position: relative; inset-inline-start: 0; z-index: 1; font-size: 1.36em; font-variation-settings: \"opsz\" 12; color: var(--ot-ink);\n    -webkit-text-stroke: 0.07em var(--ot-paper); paint-order: stroke fill;\n    transition: inset var(--ot-moderato) var(--ot-spiccato), color var(--ot-moderato);\n  }\n  .ot-avatar-family { margin-inline-start: -0.26em; font-family: var(--ot-voice); font-style: normal; font-weight: 800; font-stretch: 62%; letter-spacing: 0; color: var(--ot-ink); }\n  .ot-avatar[data-variant=\"monogram\"][data-here]::after { content: none; }\n  .ot-avatar[data-variant=\"monogram\"][data-here] .ot-avatar-given { color: var(--ot-accent); }\n  .ot-avatar[data-variant=\"monogram\"]:is(:hover, [data-force~=\"hover\"]) .ot-avatar-given { inset-inline-start: -0.16em; inset-block-start: -0.06em; }\n  .ot-avatars:has(> .ot-avatar[data-variant=\"monogram\"]) { gap: calc(var(--s, 2.75rem) * 0.3); }\n  .ot-avatars > .ot-avatar[data-variant=\"monogram\"] + .ot-avatar { margin-inline-start: 0; }\n\n  /* ── fit ── \"Less is more.\": the given name alone, set to fill one width (measured in its own face), so a short name stands\n     large and a long one small. A group stacks them between hairlines. Here now hangs an accent full stop outside the measure. */\n  .ot-avatar[data-variant=\"fit\"] {\n    display: block; width: calc(var(--s) * 1.8); height: auto; white-space: nowrap;\n    font-size: min(var(--fit, calc(var(--s) * 0.5)), calc(var(--s) * 1.1)); line-height: 1; letter-spacing: 0;\n  }\n  .ot-avatar[data-variant=\"fit\"][data-count] { font-size: var(--ot-pp); line-height: var(--ot-pp-lh); padding-top: var(--ot-space-2); }\n  .ot-avatar[data-variant=\"fit\"][data-here]::after { content: \".\" / \"\"; inset: auto; inset-inline-start: 100%; inset-block-end: 0; width: auto; aspect-ratio: auto; border-radius: 0; background: none; box-shadow: none; font-style: normal; color: var(--ot-accent); }\n  .ot-avatars:has(> .ot-avatar[data-variant=\"fit\"]) { display: inline-grid; justify-items: start; }\n  .ot-avatars > .ot-avatar[data-variant=\"fit\"] + .ot-avatar { margin-inline-start: 0; margin-top: var(--ot-space-2); padding-top: var(--ot-space-2); border-top: var(--ot-hairline) solid var(--ot-rule); width: calc(var(--s) * 1.8); }\n\n  @media (forced-colors: active) {\n    .ot-avatar-given { -webkit-text-stroke-color: Canvas; }\n    .ot-avatar[data-variant=\"monogram\"][data-here] .ot-avatar-given, .ot-avatar[data-variant=\"fit\"][data-here]::after { color: Highlight; }\n  }\n}\n",
      "type": "registry:file",
      "target": "styles/0nlytype/avatar.css"
    }
  ],
  "css": {
    "@import \"../styles/0nlytype/avatar.css\"": {}
  },
  "meta": {
    "movement": "X",
    "underneath": "native",
    "contract": "ot-avatar"
  },
  "type": "registry:ui"
}
