{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "people-grid",
  "title": "People Grid",
  "author": "ilinxa",
  "description": "Responsive grid of person cards — avatar with initials fallback, name, title, per-card links, and a custom item renderer.",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "src/registry/components/data/people-grid/people-grid.tsx",
      "content": "\"use client\";\n\nimport { memo, useId, useMemo } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  DEFAULT_PEOPLE_GRID_LABELS,\n  type PeopleGridColumns,\n  type PeopleGridLabels,\n  type PeopleGridProps,\n} from \"./types\";\nimport { PersonCard } from \"./parts/person-card\";\n\nconst COLUMNS_MAP: Record<PeopleGridColumns, string> = {\n  2: \"grid-cols-1 sm:grid-cols-2\",\n  3: \"grid-cols-1 sm:grid-cols-2 md:grid-cols-3\",\n  4: \"grid-cols-1 sm:grid-cols-2 md:grid-cols-4\",\n  5: \"grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5\",\n};\n\nfunction PeopleGridInner({\n  items,\n  heading,\n  headingAs = \"h2\",\n  columns = 3,\n  avatarSize = \"lg\",\n  alignment = \"center\",\n  linkComponent = \"a\",\n  renderItem,\n  labels: labelsProp,\n  emptyState,\n  className,\n  headingClassName,\n  gridClassName,\n  itemClassName,\n}: PeopleGridProps) {\n  const headingId = useId();\n  const HeadingTag = headingAs;\n\n  const labels = useMemo<Required<PeopleGridLabels>>(\n    () => ({ ...DEFAULT_PEOPLE_GRID_LABELS, ...labelsProp }),\n    [labelsProp],\n  );\n\n  const isEmpty = items.length === 0;\n\n  return (\n    <section\n      aria-labelledby={heading ? headingId : undefined}\n      className={cn(\"space-y-6\", className)}\n    >\n      {heading && (\n        <HeadingTag\n          id={headingId}\n          className={cn(\n            \"text-2xl font-bold text-foreground\",\n            headingClassName,\n          )}\n        >\n          {heading}\n        </HeadingTag>\n      )}\n\n      {isEmpty ? (\n        emptyState ?? (\n          <p\n            role=\"status\"\n            className=\"text-sm text-muted-foreground\"\n          >\n            {labels.emptyText}\n          </p>\n        )\n      ) : (\n        <ul\n          role=\"list\"\n          className={cn(\"grid gap-6\", COLUMNS_MAP[columns], gridClassName)}\n        >\n          {items.map((item) =>\n            renderItem ? (\n              <li key={item.id}>{renderItem(item)}</li>\n            ) : (\n              <PersonCard\n                key={item.id}\n                item={item}\n                avatarSize={avatarSize}\n                alignment={alignment}\n                linkComponent={linkComponent}\n                itemClassName={itemClassName}\n              />\n            ),\n          )}\n        </ul>\n      )}\n    </section>\n  );\n}\n\nconst PeopleGrid = memo(PeopleGridInner);\nPeopleGrid.displayName = \"PeopleGrid\";\n\nexport { PeopleGrid };\nexport default PeopleGrid;\n",
      "type": "registry:component",
      "target": "components/people-grid/people-grid.tsx"
    },
    {
      "path": "src/registry/components/data/people-grid/index.ts",
      "content": "export { PeopleGrid, default } from \"./people-grid\";\n\nexport type {\n  PeopleGridItem,\n  PeopleGridLabels,\n  PeopleGridProps,\n  PeopleGridColumns,\n  PeopleGridAvatarSize,\n  PeopleGridAlignment,\n} from \"./types\";\n\nexport { DEFAULT_PEOPLE_GRID_LABELS } from \"./types\";\n\nexport { getInitials } from \"./lib/get-initials\";\n\n",
      "type": "registry:component",
      "target": "components/people-grid/index.ts"
    },
    {
      "path": "src/registry/components/data/people-grid/types.ts",
      "content": "import type { ElementType, ReactNode } from \"react\";\n\nexport type PeopleGridColumns = 2 | 3 | 4 | 5;\nexport type PeopleGridAvatarSize = \"sm\" | \"md\" | \"lg\";\nexport type PeopleGridAlignment = \"center\" | \"start\";\n\nexport interface PeopleGridItem {\n  /** Stable identifier. Used for React keys + accessible-name composition. */\n  id: string;\n  /** Person's display name. Required — drives the initials fallback. */\n  name: string;\n  /** Role / title (muted, smaller, below name). Optional. */\n  title?: string;\n  /** Avatar image URL. Optional — falls back to initials circle. */\n  image?: string;\n  /** Image alt text. Optional — falls back to `name`. */\n  imageAlt?: string;\n  /** Optional URL — when supplied + `linkComponent` provided, the entire card becomes clickable. */\n  href?: string;\n}\n\nexport interface PeopleGridLabels {\n  /** Default: \"No people to display.\" Used when `items` is empty AND `emptyState` not provided. */\n  emptyText?: string;\n}\n\nexport interface PeopleGridProps {\n  /** Items to render in display order. */\n  items: PeopleGridItem[];\n\n  /** Optional section heading text. */\n  heading?: string;\n  /** Heading semantic level. Default: 'h2' (people grids are typically top-level page sections). */\n  headingAs?: \"h2\" | \"h3\" | \"h4\";\n\n  /** Number of columns at the largest breakpoint. Default: 3. All grids start at 1 col on mobile. */\n  columns?: PeopleGridColumns;\n  /** Avatar size. Default: 'lg' (w-24 h-24). 'md' = w-16, 'sm' = w-12. */\n  avatarSize?: PeopleGridAvatarSize;\n  /** Card alignment within each grid cell. Default: 'center'. */\n  alignment?: PeopleGridAlignment;\n\n  /** Element used when `item.href` is supplied. Default: 'a'. */\n  linkComponent?: ElementType;\n\n  /** Custom per-item renderer — bypasses the default card layout entirely. */\n  renderItem?: (item: PeopleGridItem) => ReactNode;\n\n  /** Localized labels. */\n  labels?: PeopleGridLabels;\n\n  /** Empty-state slot. Wins over `labels.emptyText`. */\n  emptyState?: ReactNode;\n\n  /** Override classes for the root <section>. */\n  className?: string;\n  /** Override classes for the heading. */\n  headingClassName?: string;\n  /** Override classes for the grid container. */\n  gridClassName?: string;\n  /** Override classes per item (the <li>). */\n  itemClassName?: string;\n}\n\n/** Default English labels — exported for consumer composition. */\nexport const DEFAULT_PEOPLE_GRID_LABELS: Required<PeopleGridLabels> = {\n  emptyText: \"No people to display.\",\n};\n",
      "type": "registry:component",
      "target": "components/people-grid/types.ts"
    },
    {
      "path": "src/registry/components/data/people-grid/parts/person-card.tsx",
      "content": "\"use client\";\n\nimport { useId, type ElementType } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport type {\n  PeopleGridAlignment,\n  PeopleGridAvatarSize,\n  PeopleGridItem,\n} from \"../types\";\nimport { getInitials } from \"../lib/get-initials\";\n\ninterface PersonCardProps {\n  item: PeopleGridItem;\n  avatarSize: PeopleGridAvatarSize;\n  alignment: PeopleGridAlignment;\n  linkComponent: ElementType;\n  itemClassName?: string;\n}\n\nconst AVATAR_SIZE_MAP: Record<\n  PeopleGridAvatarSize,\n  { wrapper: string; initials: string }\n> = {\n  sm: { wrapper: \"w-12 h-12\", initials: \"text-sm\" },\n  md: { wrapper: \"w-16 h-16\", initials: \"text-base\" },\n  lg: { wrapper: \"w-24 h-24\", initials: \"text-2xl\" },\n};\n\nexport function PersonCard({\n  item,\n  avatarSize,\n  alignment,\n  linkComponent: LinkComponent,\n  itemClassName,\n}: PersonCardProps) {\n  const nameId = useId();\n  const isLinked = item.href != null && item.href.length > 0;\n  const sizeClasses = AVATAR_SIZE_MAP[avatarSize];\n\n  return (\n    <li\n      className={cn(\n        \"relative group\",\n        alignment === \"center\" ? \"text-center\" : \"text-start\",\n        itemClassName,\n      )}\n    >\n      <div\n        className={cn(\n          \"rounded-full overflow-hidden border-4 border-primary/20 mb-3\",\n          sizeClasses.wrapper,\n          alignment === \"center\" ? \"mx-auto\" : \"\",\n        )}\n      >\n        {item.image ? (\n          <img\n            src={item.image}\n            alt={item.imageAlt ?? item.name}\n            loading=\"lazy\"\n            className=\"w-full h-full object-cover\"\n          />\n        ) : (\n          <div\n            aria-hidden=\"true\"\n            className={cn(\n              \"w-full h-full flex items-center justify-center bg-primary/10 text-primary font-semibold\",\n              sizeClasses.initials,\n            )}\n          >\n            {getInitials(item.name)}\n          </div>\n        )}\n      </div>\n\n      <h4\n        id={nameId}\n        className=\"font-semibold text-foreground motion-safe:group-hover:text-primary transition-colors\"\n      >\n        {item.name}\n      </h4>\n      {item.title && (\n        <p className=\"text-sm text-muted-foreground\">{item.title}</p>\n      )}\n\n      {isLinked && (\n        <LinkComponent\n          href={item.href}\n          aria-labelledby={nameId}\n          className=\"absolute inset-0 z-0 outline-none rounded-lg focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n        />\n      )}\n    </li>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/people-grid/parts/person-card.tsx"
    },
    {
      "path": "src/registry/components/data/people-grid/lib/get-initials.ts",
      "content": "const TITLE_PATTERN = /^(Dr|Prof|Mr|Mrs|Ms|Sr|Jr)\\.?$/i;\n\n/**\n * Pure function. Returns 1–2 uppercase initials from a name.\n *\n * Skips common honorifics (Dr., Prof., Mr., Mrs., Ms., Sr., Jr.).\n *\n * @example\n * getInitials(\"Dr. Ahmet Yılmaz\")    // → \"AY\"\n * getInitials(\"Prof. Dr. Elif Kaya\") // → \"EK\"\n * getInitials(\"Madonna\")             // → \"M\"\n * getInitials(\"\")                    // → \"?\"\n */\nexport function getInitials(name: string): string {\n  const trimmed = name.trim();\n  if (!trimmed) return \"?\";\n\n  const parts = trimmed\n    .split(/\\s+/)\n    .filter((p) => !TITLE_PATTERN.test(p));\n\n  if (parts.length === 0) {\n    return trimmed.charAt(0).toUpperCase() || \"?\";\n  }\n  if (parts.length === 1) {\n    return parts[0].charAt(0).toUpperCase();\n  }\n  return (\n    parts[0].charAt(0) + parts[parts.length - 1].charAt(0)\n  ).toUpperCase();\n}\n",
      "type": "registry:component",
      "target": "components/people-grid/lib/get-initials.ts"
    }
  ],
  "categories": [
    "data"
  ],
  "type": "registry:block"
}