{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "historical-figure-card",
  "type": "registry:component",
  "title": "Historical Figure Card",
  "description": "Profile card with portrait, lifespan timeline, fields, works, quote, connections, and an expandable biography section.",
  "dependencies": [
    "@vllnt/ui@^0.2.1",
    "lucide-react"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/historical-figure-card/historical-figure-card.tsx",
      "content": "\"use client\";\n\nimport {\n  type ComponentPropsWithoutRef,\n  forwardRef,\n  type ReactNode,\n  useCallback,\n  useState,\n} from \"react\";\n\nimport { ChevronDown, User } from \"lucide-react\";\n\nimport { cn } from \"@vllnt/ui\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"@vllnt/ui\";\nimport { Badge } from \"@vllnt/ui\";\n\nconst FALLBACK_LIFESPAN_MIN_YEAR = 1500;\nconst FALLBACK_LIFESPAN_SPAN_YEARS = 100;\n\n/**\n * Birth or death record for {@link HistoricalFigureCardProps}.\n *\n * @public\n */\nexport type HistoricalFigureCardLifeEvent = {\n  /** Optional secondary line (e.g. \"Vinci, Italy\"). */\n  place?: ReactNode;\n  /**\n   * Year as a positive integer for AD / a negative integer for BC.\n   * Pass `undefined` for unknown.\n   */\n  year?: number;\n};\n\n/**\n * Connection between this figure and another.\n *\n * @public\n */\nexport type HistoricalFigureCardConnection = {\n  /** Optional URL for the connected figure's profile. */\n  href?: string;\n  /** Connected figure's display name. */\n  name: ReactNode;\n  /** Free-form relation label (e.g. \"Patron\", \"Contemporary/rival\"). */\n  relation: ReactNode;\n};\n\n/**\n * Pull-quote with attribution for {@link HistoricalFigureCardProps}.\n *\n * @public\n */\nexport type HistoricalFigureCardQuote = {\n  /** Source/citation for the quote (book, letter, year). */\n  source?: ReactNode;\n  /** Quote text. */\n  text: ReactNode;\n};\n\n/**\n * Localizable strings for the bio toggle button.\n *\n * @public\n */\nexport type HistoricalFigureCardLabels = {\n  /** Caption for the bio toggle when expanded. Defaults to `\"Hide biography\"`. */\n  collapseBio?: string;\n  /** Heading rendered above the connections list. Defaults to `\"Connections\"`. */\n  connections?: string;\n  /** Caption for the bio toggle when collapsed. Defaults to `\"Read biography\"`. */\n  expandBio?: string;\n  /** Heading rendered above the fields list. Defaults to `\"Fields\"`. */\n  fields?: string;\n  /** Aria-label for the lifespan timeline bar. Defaults to `\"Lifespan\"`. */\n  lifespan?: string;\n  /** Heading rendered above the works list. Defaults to `\"Notable works\"`. */\n  works?: string;\n};\n\n/**\n * Props for {@link HistoricalFigureCard}.\n *\n * @public\n */\nexport type HistoricalFigureCardProps = {\n  /** Optional expandable biography content. */\n  biography?: ReactNode;\n  /** Birth event (year + optional place). */\n  birth?: HistoricalFigureCardLifeEvent;\n  /** Connections / relationships to other figures. */\n  connections?: HistoricalFigureCardConnection[];\n  /** Death event (year + optional place). */\n  death?: HistoricalFigureCardLifeEvent;\n  /** Era label, rendered as a Badge. */\n  era?: ReactNode;\n  /** Fields / domains tags (e.g. \"Art\", \"Anatomy\"). */\n  fields?: ReactNode[];\n  /** Localizable captions. */\n  labels?: HistoricalFigureCardLabels;\n  /** Display name. */\n  name: ReactNode;\n  /** Portrait image src. Falls back to a silhouette when omitted. */\n  portrait?: string;\n  /** Optional URL pointing to the full profile. */\n  profileHref?: string;\n  /** Optional pull-quote with attribution. */\n  quote?: HistoricalFigureCardQuote;\n  /** Optional descriptor under the name (e.g. \"Polymath\"). */\n  title?: ReactNode;\n  /** Notable works list. */\n  works?: ReactNode[];\n} & ComponentPropsWithoutRef<\"article\">;\n\nconst DEFAULT_LABELS = {\n  collapseBio: \"Hide biography\",\n  connections: \"Connections\",\n  expandBio: \"Read biography\",\n  fields: \"Fields\",\n  lifespan: \"Lifespan\",\n  works: \"Notable works\",\n} as const satisfies Required<HistoricalFigureCardLabels>;\n\nfunction formatYear(year: number | undefined): string | undefined {\n  if (year === undefined) return undefined;\n  if (year < 0) return `${Math.abs(year).toString()} BC`;\n  return year.toString();\n}\n\nfunction getInitials(name: ReactNode): string {\n  if (typeof name !== \"string\") return \"\";\n  const parts = name.trim().split(/\\s+/).slice(0, 2);\n  return parts\n    .map((part) => part.charAt(0).toUpperCase())\n    .join(\"\")\n    .slice(0, 2);\n}\n\ntype LifespanBarProps = {\n  birthYear?: number;\n  deathYear?: number;\n  label: string;\n};\n\nfunction LifespanBar({\n  birthYear,\n  deathYear,\n  label,\n}: LifespanBarProps): ReactNode {\n  if (birthYear === undefined || deathYear === undefined) return null;\n  if (deathYear <= birthYear) return null;\n\n  const span = deathYear - birthYear;\n  const min = Math.min(birthYear, FALLBACK_LIFESPAN_MIN_YEAR);\n  const range =\n    Math.max(deathYear, min + FALLBACK_LIFESPAN_SPAN_YEARS) - min || 1;\n  const start = ((birthYear - min) / range) * 100;\n  const width = (span / range) * 100;\n\n  return (\n    <div\n      aria-label={`${label}: ${formatYear(birthYear) ?? \"\"} – ${formatYear(deathYear) ?? \"\"}`}\n      className=\"relative mt-2 h-1.5 w-full rounded-full bg-muted\"\n      role=\"img\"\n    >\n      <span\n        className=\"absolute h-full rounded-full bg-primary\"\n        style={{ left: `${start.toString()}%`, width: `${width.toString()}%` }}\n      />\n    </div>\n  );\n}\n\ntype LifeEventLineProps = {\n  caption: string;\n  event?: HistoricalFigureCardLifeEvent;\n};\n\nfunction LifeEventLine({ caption, event }: LifeEventLineProps): ReactNode {\n  if (!event) return null;\n  const year = formatYear(event.year);\n  if (year === undefined && !event.place) return null;\n  return (\n    <p className=\"text-xs text-muted-foreground\">\n      <span className=\"font-semibold text-foreground\">{caption}</span>{\" \"}\n      {year ?? \"Unknown\"}\n      {event.place ? <span> · {event.place}</span> : null}\n    </p>\n  );\n}\n\ntype FigureChipsProps = {\n  heading: string;\n  items: ReactNode[];\n};\n\nfunction FigureChips({ heading, items }: FigureChipsProps): ReactNode {\n  if (items.length === 0) return null;\n  return (\n    <div className=\"flex flex-col gap-2\">\n      <h4 className=\"text-xs font-semibold uppercase tracking-wide text-muted-foreground\">\n        {heading}\n      </h4>\n      <div className=\"flex flex-wrap gap-1.5\">\n        {items.map((item, index) => (\n          <Badge key={`chip-${index.toString()}`} variant=\"secondary\">\n            {item}\n          </Badge>\n        ))}\n      </div>\n    </div>\n  );\n}\n\ntype FigureWorksListProps = {\n  heading: string;\n  items: ReactNode[];\n};\n\nfunction FigureWorksList({ heading, items }: FigureWorksListProps): ReactNode {\n  if (items.length === 0) return null;\n  return (\n    <div className=\"flex flex-col gap-2\">\n      <h4 className=\"text-xs font-semibold uppercase tracking-wide text-muted-foreground\">\n        {heading}\n      </h4>\n      <ul className=\"flex flex-col gap-1 text-sm text-foreground\">\n        {items.map((item, index) => (\n          <li className=\"leading-tight\" key={`work-${index.toString()}`}>\n            {item}\n          </li>\n        ))}\n      </ul>\n    </div>\n  );\n}\n\ntype FigureConnectionsProps = {\n  heading: string;\n  items: HistoricalFigureCardConnection[];\n};\n\nfunction FigureConnections({\n  heading,\n  items,\n}: FigureConnectionsProps): ReactNode {\n  if (items.length === 0) return null;\n  return (\n    <div className=\"flex flex-col gap-2\">\n      <h4 className=\"text-xs font-semibold uppercase tracking-wide text-muted-foreground\">\n        {heading}\n      </h4>\n      <ul className=\"flex flex-col gap-1.5 text-sm\">\n        {items.map((connection, index) => {\n          const labelNode = connection.href ? (\n            <a\n              className=\"font-medium text-foreground underline-offset-4 hover:underline\"\n              href={connection.href}\n            >\n              {connection.name}\n            </a>\n          ) : (\n            <span className=\"font-medium text-foreground\">\n              {connection.name}\n            </span>\n          );\n          return (\n            <li\n              className=\"flex items-baseline justify-between gap-3\"\n              key={`connection-${index.toString()}`}\n            >\n              {labelNode}\n              <span className=\"text-xs text-muted-foreground\">\n                {connection.relation}\n              </span>\n            </li>\n          );\n        })}\n      </ul>\n    </div>\n  );\n}\n\ntype FigureQuoteProps = {\n  quote: HistoricalFigureCardQuote;\n};\n\nfunction FigureQuote({ quote }: FigureQuoteProps): ReactNode {\n  return (\n    <blockquote className=\"border-l-2 border-primary/40 pl-3 text-sm italic text-muted-foreground\">\n      “{quote.text}”\n      {quote.source ? (\n        <footer className=\"mt-1 text-xs not-italic text-muted-foreground/80\">\n          {quote.source}\n        </footer>\n      ) : null}\n    </blockquote>\n  );\n}\n\ntype FigureBioProps = {\n  biography: ReactNode;\n  collapseLabel: string;\n  expandLabel: string;\n};\n\nfunction FigureBio({\n  biography,\n  collapseLabel,\n  expandLabel,\n}: FigureBioProps): ReactNode {\n  const [open, setOpen] = useState(false);\n  const handleToggle = useCallback(() => {\n    setOpen((value) => !value);\n  }, []);\n\n  return (\n    <div className=\"flex flex-col gap-2\">\n      <button\n        aria-expanded={open}\n        className=\"inline-flex w-fit items-center gap-1 text-sm font-medium text-primary underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n        onClick={handleToggle}\n        type=\"button\"\n      >\n        {open ? collapseLabel : expandLabel}\n        <ChevronDown\n          aria-hidden=\"true\"\n          className={cn(\n            \"size-4 transition-transform\",\n            open ? \"rotate-180\" : \"rotate-0\",\n          )}\n        />\n      </button>\n      {open ? (\n        <div className=\"text-sm leading-relaxed text-foreground\">\n          {biography}\n        </div>\n      ) : null}\n    </div>\n  );\n}\n\n/**\n * Profile card for historical figures with portrait, lifespan timeline,\n * fields / works / connections, optional pull-quote, and an expandable\n * biography section. Composes Avatar and Badge.\n *\n * @example\n * ```tsx\n * <HistoricalFigureCard\n *   name=\"Leonardo da Vinci\"\n *   title=\"Polymath\"\n *   era=\"Renaissance\"\n *   birth={{ year: 1452, place: \"Vinci, Italy\" }}\n *   death={{ year: 1519, place: \"Amboise, France\" }}\n *   fields={[\"Art\", \"Science\"]}\n *   works={[\"Mona Lisa\", \"Vitruvian Man\"]}\n *   quote={{ text: \"Learning never exhausts the mind.\", source: \"Notebooks\" }}\n *   profileHref=\"/figures/da-vinci\"\n * />\n * ```\n *\n * @public\n */\ntype FigureHeaderProps = {\n  era?: ReactNode;\n  name: ReactNode;\n  portrait?: string;\n  title?: ReactNode;\n};\n\nfunction FigureHeader({\n  era,\n  name,\n  portrait,\n  title,\n}: FigureHeaderProps): ReactNode {\n  const initials = getInitials(name);\n  const altName = typeof name === \"string\" ? name : undefined;\n  return (\n    <header className=\"flex items-start gap-4\">\n      <Avatar className=\"size-14 shrink-0 ring-2 ring-border\">\n        {portrait ? <AvatarImage alt={altName} src={portrait} /> : null}\n        <AvatarFallback className=\"text-sm\">\n          {initials || <User aria-hidden=\"true\" className=\"size-5\" />}\n        </AvatarFallback>\n      </Avatar>\n      <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n        <h3 className=\"text-base font-semibold leading-tight tracking-tight\">\n          {name}\n        </h3>\n        {title ? (\n          <p className=\"text-sm text-muted-foreground\">{title}</p>\n        ) : null}\n        {era ? (\n          <Badge className=\"self-start\" variant=\"outline\">\n            {era}\n          </Badge>\n        ) : null}\n      </div>\n    </header>\n  );\n}\n\ntype FigureLifeBlockProps = {\n  birth?: HistoricalFigureCardLifeEvent;\n  death?: HistoricalFigureCardLifeEvent;\n  lifespanLabel: string;\n};\n\nfunction FigureLifeBlock({\n  birth,\n  death,\n  lifespanLabel,\n}: FigureLifeBlockProps): ReactNode {\n  return (\n    <div className=\"flex flex-col gap-1\">\n      <LifeEventLine caption=\"Born\" event={birth} />\n      <LifeEventLine caption=\"Died\" event={death} />\n      <LifespanBar\n        birthYear={birth?.year}\n        deathYear={death?.year}\n        label={lifespanLabel}\n      />\n    </div>\n  );\n}\n\nexport const HistoricalFigureCard = forwardRef<\n  HTMLElement,\n  HistoricalFigureCardProps\n>((props, ref) => {\n  const {\n    biography,\n    birth,\n    className,\n    connections,\n    death,\n    era,\n    fields,\n    labels,\n    name,\n    portrait,\n    profileHref,\n    quote,\n    title,\n    works,\n    ...rest\n  } = props;\n\n  const resolvedLabels = { ...DEFAULT_LABELS, ...labels };\n\n  return (\n    <article\n      className={cn(\n        \"flex flex-col gap-4 rounded-2xl border bg-background p-5 text-foreground shadow-sm\",\n        className,\n      )}\n      ref={ref}\n      {...rest}\n    >\n      <FigureHeader era={era} name={name} portrait={portrait} title={title} />\n\n      <FigureLifeBlock\n        birth={birth}\n        death={death}\n        lifespanLabel={resolvedLabels.lifespan}\n      />\n\n      {fields && fields.length > 0 ? (\n        <FigureChips heading={resolvedLabels.fields} items={fields} />\n      ) : null}\n\n      {works && works.length > 0 ? (\n        <FigureWorksList heading={resolvedLabels.works} items={works} />\n      ) : null}\n\n      {quote ? <FigureQuote quote={quote} /> : null}\n\n      {connections && connections.length > 0 ? (\n        <FigureConnections\n          heading={resolvedLabels.connections}\n          items={connections}\n        />\n      ) : null}\n\n      {biography ? (\n        <FigureBio\n          biography={biography}\n          collapseLabel={resolvedLabels.collapseBio}\n          expandLabel={resolvedLabels.expandBio}\n        />\n      ) : null}\n\n      {profileHref ? (\n        <a\n          className=\"text-sm font-medium text-primary underline-offset-4 hover:underline\"\n          href={profileHref}\n        >\n          View full profile →\n        </a>\n      ) : null}\n    </article>\n  );\n});\nHistoricalFigureCard.displayName = \"HistoricalFigureCard\";\n",
      "type": "registry:component"
    }
  ],
  "version": "0.2.1",
  "stability": "stable"
}
