{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "role-map",
  "title": "Role Map",
  "description": "Who did what on a project: panels, quiet rows or a table of people against areas, with one person in the accent.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/miseui/ui/role-map.tsx",
      "content": "import type * as React from \"react\"\r\nimport { useState } from \"react\"\r\nimport { RoleMatrix, RolePanels, RoleRows, collectAreas, type RoleFocus, type RoleMapVariant, type RoleMember } from \"@/registry/miseui/ui/role-map-views\"\r\nimport { cn } from \"@/registry/miseui/lib/utils\"\r\n\r\ntype RoleMapEffect = \"focus\"\r\n\r\n/** Hovering previews and pressing pins. On touch only the pin exists, which is why there are two states. */\r\nfunction useRoleFocus(enabled: boolean): RoleFocus {\r\n  const [hovered, setHovered] = useState<string | null>(null)\r\n  const [pinned, setPinned] = useState<string | null>(null)\r\n  return {\r\n    enabled,\r\n    active: enabled ? (hovered ?? pinned) : null,\r\n    pinned,\r\n    hover: setHovered,\r\n    toggle: (name) => setPinned(pinned === name ? null : name),\r\n  }\r\n}\r\n\r\ntype RoleMapProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\r\n  /** Who did what. Mark at most one as `lead`: it is the focal point of the map. */\r\n  members: readonly RoleMember[]\r\n  /** `panels` a surface per person, `rows` one quiet line per person, `matrix` a table of who touched which area. */\r\n  variant?: RoleMapVariant\r\n  /** `focus` dims everyone else when a person is pointed at or pressed, to trace one person's share. */\r\n  effect?: RoleMapEffect\r\n  /** Shown for someone with nothing listed, so an empty share reads as a fact and not as a bug. */\r\n  emptyText?: string\r\n  /** Names the matrix table and the first column. */\r\n  label?: string\r\n}\r\n\r\nfunction RoleMap({ members, variant = \"panels\", effect, emptyText = \"Nothing recorded\", label = \"Who did what\", className, ...props }: RoleMapProps) {\r\n  const focus = useRoleFocus(effect === \"focus\")\r\n  const shared = { members, focus, emptyText }\r\n  return (\r\n    <div data-slot=\"role-map\" data-variant={variant} data-effect={effect} className={cn(\"@container grid w-full min-w-0 grid-cols-[minmax(0,1fr)]\", className)} {...props}>\r\n      {variant === \"panels\" ? <RolePanels {...shared} /> : null}\r\n      {variant === \"rows\" ? <RoleRows {...shared} /> : null}\r\n      {variant === \"matrix\" ? <RoleMatrix {...shared} label={label} /> : null}\r\n    </div>\r\n  )\r\n}\r\n\r\nexport { RoleMap, collectAreas, type RoleMapProps, type RoleMapVariant, type RoleMapEffect, type RoleMember }\r\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/role-map-views.tsx",
      "content": "import type * as React from \"react\"\nimport { Check } from \"lucide-react\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\ntype RoleMapVariant = \"panels\" | \"rows\" | \"matrix\"\n\ninterface RoleMember {\n  name: string\n  /** What they are, such as \"Product design\". */\n  role?: string\n  /** The areas they took on. */\n  owns: readonly string[]\n  /** The person this map is about. Gets the accent. */\n  lead?: boolean\n}\n\ninterface RoleFocus {\n  enabled: boolean\n  active: string | null\n  pinned: string | null\n  hover: (name: string | null) => void\n  toggle: (name: string) => void\n}\n\ninterface ViewProps {\n  members: readonly RoleMember[]\n  focus: RoleFocus\n  emptyText: string\n}\n\n/** First appearance sets the order, so the table reads in the order the data was written. */\nfunction collectAreas(members: readonly RoleMember[]) {\n  return [...new Set(members.flatMap((member) => member.owns))]\n}\n\nconst DIM = \"transition-opacity duration-(--duration-base) ease-(--ease-mise) motion-reduce:transition-none\"\n\nfunction dimmed(focus: RoleFocus, name: string) {\n  return focus.active !== null && focus.active !== name\n}\n\nfunction MemberName({ member, focus, className }: { member: RoleMember; focus: RoleFocus; className?: string }) {\n  const base = cn(\"min-w-0 text-start text-sm leading-5 font-medium wrap-break-word\", className)\n  if (!focus.enabled) return <span className={base}>{member.name}</span>\n  return (\n    <button\n      type=\"button\"\n      aria-pressed={focus.pinned === member.name}\n      data-slot=\"role-map-person\"\n      className={cn(base, \"-mx-1 rounded-sm px-1 underline-offset-4 outline-none hover:underline focus-visible:ring-[3px] focus-visible:ring-ring/50 pointer-coarse:min-h-10\")}\n      onClick={() => focus.toggle(member.name)}\n    >\n      {member.name}\n    </button>\n  )\n}\n\n/** Mouse only: a tap also fires enter and leave, which would fight the pin it is about to set. */\nfunction hoverProps(focus: RoleFocus, name: string): React.HTMLAttributes<HTMLElement> {\n  if (!focus.enabled) return {}\n  return {\n    onPointerEnter: (event: React.PointerEvent) => {\n      if (event.pointerType !== \"touch\") focus.hover(name)\n    },\n    onPointerLeave: () => focus.hover(null),\n  }\n}\n\nfunction RolePanels({ members, focus, emptyText }: ViewProps) {\n  return (\n    <ul data-slot=\"role-map-panels\" className=\"grid min-w-0 gap-2 @md:grid-cols-2\">\n      {members.map((member) => (\n        <li\n          key={member.name}\n          data-slot=\"role-map-member\"\n          data-lead={member.lead ? \"true\" : undefined}\n          data-dimmed={dimmed(focus, member.name) ? \"true\" : undefined}\n          className={cn(\"min-w-0 rounded-lg p-4\", DIM, member.lead ? \"border border-transparent bg-mise-soft\" : \"border\", dimmed(focus, member.name) && \"opacity-40\")}\n          {...hoverProps(focus, member.name)}\n        >\n          <MemberName member={member} focus={focus} />\n          {member.role ? <p className=\"text-[13px] leading-5 text-muted-foreground\">{member.role}</p> : null}\n          {member.owns.length > 0 ? (\n            <ul className=\"mt-3 flex flex-wrap gap-1\">\n              {member.owns.map((area) => (\n                <li key={area} className={cn(\"rounded-sm px-2 py-1 text-[13px] leading-5\", member.lead ? \"bg-background/70\" : \"bg-muted text-muted-foreground\")}>\n                  {area}\n                </li>\n              ))}\n            </ul>\n          ) : (\n            <p className=\"mt-3 text-[13px] leading-5 text-muted-foreground\">{emptyText}</p>\n          )}\n        </li>\n      ))}\n    </ul>\n  )\n}\n\nfunction RoleRows({ members, focus, emptyText }: ViewProps) {\n  return (\n    <ul data-slot=\"role-map-rows\" className=\"grid min-w-0 grid-cols-[minmax(0,1fr)]\">\n      {members.map((member) => (\n        <li\n          key={member.name}\n          data-slot=\"role-map-member\"\n          data-lead={member.lead ? \"true\" : undefined}\n          data-dimmed={dimmed(focus, member.name) ? \"true\" : undefined}\n          className={cn(\"grid min-w-0 gap-x-4 gap-y-1 border-b py-3 last:border-b-0 @md:grid-cols-[minmax(0,10rem)_minmax(0,1fr)]\", DIM, dimmed(focus, member.name) && \"opacity-40\")}\n          {...hoverProps(focus, member.name)}\n        >\n          <span className=\"grid min-w-0 justify-items-start\">\n            <MemberName member={member} focus={focus} />\n            {member.role ? <span className=\"text-[13px] leading-5 text-muted-foreground\">{member.role}</span> : null}\n          </span>\n          <span className={cn(\"min-w-0 text-[13px] leading-5 text-pretty\", member.lead ? \"text-foreground\" : \"text-muted-foreground\")}>\n            {member.owns.length > 0 ? member.owns.join(\", \") : emptyText}\n          </span>\n        </li>\n      ))}\n    </ul>\n  )\n}\n\nfunction MatrixCell({ member, area, focus }: { member: RoleMember; area: string; focus: RoleFocus }) {\n  const owned = member.owns.includes(area)\n  return (\n    <td data-slot=\"role-map-cell\" data-dimmed={dimmed(focus, member.name) ? \"true\" : undefined} className={cn(\"border-t py-2 text-center\", DIM, member.lead && \"bg-mise-soft\", dimmed(focus, member.name) && \"opacity-40\")}>\n      {owned ? <Check aria-hidden=\"true\" className=\"mx-auto size-4\" /> : null}\n      <span className=\"sr-only\">{owned ? `${member.name}: yes` : `${member.name}: no`}</span>\n    </td>\n  )\n}\n\nfunction RoleMatrix({ members, focus, label }: Omit<ViewProps, \"emptyText\"> & { label: string }) {\n  const areas = collectAreas(members)\n  return (\n    // Fixed layout makes the columns share the width, so a narrow cell squeezes the names instead of overflowing.\n    <table data-slot=\"role-map-matrix\" className=\"w-full min-w-0 table-fixed border-collapse text-[13px] leading-5\">\n      <caption className=\"sr-only\">{label}</caption>\n      <thead>\n        <tr>\n          <th scope=\"col\" className=\"w-[34%] py-2 pe-2 text-start font-normal text-muted-foreground\">\n            <span className=\"sr-only\">Area</span>\n          </th>\n          {members.map((member) => (\n            <th key={member.name} scope=\"col\" className={cn(\"px-1 py-2 align-bottom text-center\", DIM, member.lead && \"rounded-t-sm bg-mise-soft\", dimmed(focus, member.name) && \"opacity-40\")} {...hoverProps(focus, member.name)}>\n              <MemberName member={member} focus={focus} className=\"text-center text-[13px]\" />\n            </th>\n          ))}\n        </tr>\n      </thead>\n      <tbody>\n        {areas.map((area) => (\n          <tr key={area}>\n            <th scope=\"row\" className=\"border-t py-2 pe-2 text-start font-normal wrap-break-word text-muted-foreground\">\n              {area}\n            </th>\n            {members.map((member) => (\n              <MatrixCell key={member.name} member={member} area={area} focus={focus} />\n            ))}\n          </tr>\n        ))}\n      </tbody>\n    </table>\n  )\n}\n\nexport { RoleMatrix, RolePanels, RoleRows, collectAreas, type RoleFocus, type RoleMapVariant, type RoleMember }\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "display"
  ],
  "type": "registry:ui"
}