{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "work-experience",
  "title": "Work Experience",
  "author": "Om Patil <patilom1906@gmail.com>",
  "description": "Display work experiences with role details, company logos, and durations.",
  "dependencies": [
    "react-markdown",
    "date-fns",
    "lucide-react"
  ],
  "registryDependencies": [
    "collapsible",
    "separator",
    "https://heyompatil.me/r/chevrons-up-down-icon.json",
    "https://heyompatil.me/r/typography.json"
  ],
  "files": [
    {
      "path": "src/registry/components/work-experience/work-experience.tsx",
      "content": "\"use client\"\n\nimport { useCallback, useRef, type ComponentProps } from \"react\"\nimport { differenceInMonths, parse } from \"date-fns\"\nimport { BriefcaseBusinessIcon, InfinityIcon } from \"lucide-react\"\nimport ReactMarkdown from \"react-markdown\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n  Collapsible,\n  CollapsibleContent,\n  CollapsibleTrigger,\n} from \"@/components/ui/collapsible\"\nimport { Separator } from \"@/components/ui/separator\"\nimport type { ChevronsUpDownIconHandle } from \"@/registry/components/chevrons-up-down-icon\"\nimport { ChevronsUpDownIcon } from \"@/registry/components/chevrons-up-down-icon\"\n\nexport type ExperiencePositionItemType = {\n  /** Unique identifier for the position */\n  id: string\n  /** The job title or position name */\n  title: string\n  /**\n   * Employment period of the position.\n   * Use \"MM.YYYY\" or \"YYYY\" format. Omit `end` for current roles.\n   */\n  employmentPeriod: {\n    /** Start date (e.g., \"10.2022\" or \"2020\"). */\n    start: string\n    /** End date; leave undefined for \"Present\". */\n    end?: string\n  }\n  /** The type of employment (e.g., \"Full-time\", \"Part-time\", \"Contract\") */\n  employmentType?: string\n  /** A brief description of the position or responsibilities */\n  description?: string\n  /** An icon representing the position */\n  icon?: React.ReactElement\n  /** A list of skills associated with the position */\n  skills?: string[]\n  /** Indicates if the position details are expanded in the UI */\n  isExpanded?: boolean\n}\n\nexport type ExperienceItemType = {\n  /** Unique identifier for the experience item */\n  id: string\n  /** Name of the company where the experience was gained */\n  companyName: string\n  /** URL or path to the company's logo image */\n  companyLogo?: string\n  /** URL to the company's website. */\n  companyWebsite?: string\n  /**\n   * List of positions held at the company\n   * @fumadocsHref #experiencepositionitemtype\n   * */\n  positions: ExperiencePositionItemType[]\n  /** Indicates if this is the user's current employer */\n  isCurrentEmployer?: boolean\n}\n\nexport type WorkExperienceProps = {\n  className?: string\n  /** @fumadocsHref #experienceitemtype */\n  experiences: ExperienceItemType[]\n}\n\nexport function WorkExperience({\n  className,\n  experiences,\n}: WorkExperienceProps) {\n  return (\n    <div className={cn(\"bg-background px-4 text-foreground\", className)}>\n      {experiences.map((experience) => (\n        <ExperienceItem key={experience.id} experience={experience} />\n      ))}\n    </div>\n  )\n}\n\nexport type ExperienceItemProps = {\n  experience: ExperienceItemType\n}\n\nexport function ExperienceItem({ experience }: ExperienceItemProps) {\n  return (\n    <div className=\"space-y-4 py-4\">\n      <div className=\"not-prose flex items-center gap-3\">\n        <div className=\"flex size-6 shrink-0 items-center justify-center\">\n          {experience.companyLogo ? (\n            <img\n              src={experience.companyLogo}\n              alt={experience.companyName}\n              className=\"size-6 rounded-full\"\n              aria-hidden\n            />\n          ) : (\n            <span className=\"flex size-2 rounded-full bg-zinc-300 dark:bg-zinc-600\" />\n          )}\n        </div>\n\n        <h3 className=\"text-lg/snug font-semibold\">\n          {experience.companyWebsite ? (\n            <a\n              className=\"link\"\n              href={experience.companyWebsite}\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n            >\n              {experience.companyName}\n            </a>\n          ) : (\n            experience.companyName\n          )}\n        </h3>\n\n        {experience.isCurrentEmployer && (\n          <span\n            className=\"relative flex items-center justify-center\"\n            aria-label=\"Current Employer\"\n          >\n            <span className=\"absolute inline-flex size-3 animate-ping rounded-full bg-sky-500 opacity-50\" />\n            <span className=\"relative inline-flex size-2 rounded-full bg-sky-500\" />\n          </span>\n        )}\n      </div>\n\n      <div className=\"relative space-y-4 before:absolute before:left-3 before:h-full before:w-px before:bg-border\">\n        {experience.positions.map((position) => (\n          <ExperiencePositionItem key={position.id} position={position} />\n        ))}\n      </div>\n    </div>\n  )\n}\n\nexport type ExperiencePositionItemProps = {\n  position: ExperiencePositionItemType\n}\n\nexport function ExperiencePositionItem({\n  position,\n}: ExperiencePositionItemProps) {\n  const chevronsUpDownIconRef = useRef<ChevronsUpDownIconHandle>(null)\n\n  const handleOpenChange = useCallback((open: boolean) => {\n    const controls = chevronsUpDownIconRef.current\n    if (!controls) return\n\n    if (open) {\n      controls.startAnimation()\n    } else {\n      controls.stopAnimation()\n    }\n  }, [])\n\n  const { start, end } = position.employmentPeriod\n  const isOngoing = !end\n  const duration = formatDuration(start, end)\n\n  return (\n    <Collapsible\n      className=\"relative last:before:absolute last:before:h-full last:before:w-4 last:before:bg-background\"\n      defaultOpen={position.isExpanded}\n      onOpenChange={handleOpenChange}\n      disabled={!position.description}\n    >\n      <CollapsibleTrigger\n        className={cn(\n          \"group/experience-position not-prose block w-full text-left select-none\",\n          \"relative before:absolute before:-top-1 before:-right-1 before:-bottom-1.5 before:left-7 before:rounded-lg hover:before:bg-muted/30\",\n          \"data-disabled:before:content-none\"\n        )}\n      >\n        <div className=\"relative z-1 mb-1 flex items-start gap-3 text-base\">\n          <div\n            className={cn(\n              \"flex size-6 shrink-0 items-center justify-center rounded-lg\",\n              \"bg-muted text-muted-foreground\",\n              \"border border-muted-foreground/15 ring-1 ring-line ring-offset-1 ring-offset-background\",\n              \"[&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n            )}\n          >\n            {position.icon ?? <BriefcaseBusinessIcon />}\n          </div>\n\n          <h4 className=\"flex-1 font-medium text-balance text-foreground\">\n            {position.title}\n          </h4>\n\n          <div className=\"shrink-0 text-muted-foreground group-data-disabled/experience-position:hidden [&_svg]:h-lh [&_svg]:w-4\">\n            <ChevronsUpDownIcon ref={chevronsUpDownIconRef} duration={0.15} />\n          </div>\n        </div>\n\n        {/* Separators are aria-hidden: a dl may only expose dt/dd groups, and these dividers are decorative. */}\n        <dl className=\"relative z-1 flex items-center gap-2 pl-9 text-sm text-muted-foreground\">\n          {position.employmentType && (\n            <>\n              <div>\n                <dt className=\"sr-only\">Employment Type</dt>\n                <dd>{position.employmentType}</dd>\n              </div>\n\n              <Separator\n                className=\"data-vertical:h-4 data-vertical:self-center\"\n                orientation=\"vertical\"\n                aria-hidden\n              />\n            </>\n          )}\n\n          <div>\n            <dt className=\"sr-only\">Employment Period</dt>\n            <dd className=\"flex items-center gap-0.5 tabular-nums\">\n              <span>{start}</span>\n              <span className=\"font-mono\">—</span>\n              {isOngoing ? (\n                <InfinityIcon\n                  className=\"size-4.5 translate-y-[0.5px]\"\n                  aria-label=\"Present\"\n                />\n              ) : (\n                <span>{end}</span>\n              )}\n            </dd>\n          </div>\n\n          {duration && (\n            <>\n              <Separator\n                className=\"data-vertical:h-4 data-vertical:self-center\"\n                orientation=\"vertical\"\n                aria-hidden\n              />\n              <div>\n                <dt className=\"sr-only\">Duration</dt>\n                <dd className=\"tabular-nums\">{duration}</dd>\n              </div>\n            </>\n          )}\n        </dl>\n      </CollapsibleTrigger>\n\n      <CollapsibleContent className=\"overflow-hidden\">\n        {position.description && (\n          <Prose className=\"pt-2 pl-9\">\n            <ReactMarkdown>{position.description}</ReactMarkdown>\n          </Prose>\n        )}\n      </CollapsibleContent>\n\n      {Array.isArray(position.skills) && position.skills.length > 0 && (\n        <ul className=\"not-prose flex flex-wrap gap-1.5 pt-3 pl-9\">\n          {position.skills.map((skill, index) => (\n            <li key={index} className=\"flex\">\n              <Skill>{skill}</Skill>\n            </li>\n          ))}\n        </ul>\n      )}\n    </Collapsible>\n  )\n}\n\nfunction Prose({ className, ...props }: ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"prose max-w-none prose-ncdai prose-zinc dark:prose-invert\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction Skill({ className, ...props }: ComponentProps<\"span\">) {\n  return (\n    <span\n      className={cn(\n        \"inline-flex items-center rounded-md border bg-muted/50 px-1.5 py-0.5 font-mono text-xs text-muted-foreground\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction formatDuration(start: string, end?: string): string {\n  const startHasMonth = start.includes(\".\")\n  const endHasMonth = end ? end.includes(\".\") : true\n\n  // Both year-only: granularity is years, no month arithmetic needed.\n  if (!startHasMonth && end && !endHasMonth) {\n    const years = parseInt(end, 10) - parseInt(start, 10)\n    if (years <= 0) {\n      return \"\"\n    }\n    return `${years}y`\n  }\n\n  const startDate = parsePeriodDate(start, \"first\")\n  const endDate = end ? parsePeriodDate(end, \"last\") : new Date()\n\n  // +1 to count both the start and end months inclusively.\n  const totalMonths = differenceInMonths(endDate, startDate) + 1\n  if (totalMonths <= 0) {\n    return \"\"\n  }\n\n  if (totalMonths < 12) {\n    return `${totalMonths}m`\n  }\n\n  const years = Math.floor(totalMonths / 12)\n  const months = totalMonths % 12\n  if (months === 0) {\n    return `${years}y`\n  }\n  return `${years}y ${months}m`\n}\n\nfunction parsePeriodDate(str: string, fallbackMonth: \"first\" | \"last\"): Date {\n  if (str.includes(\".\")) {\n    return parse(str, \"MM.yyyy\", new Date())\n  }\n  return parse(\n    `${fallbackMonth === \"last\" ? \"12\" : \"01\"}.${str}`,\n    \"MM.yyyy\",\n    new Date()\n  )\n}\n\n// Om Patil portfolio\n",
      "type": "registry:component",
      "target": "@components/work-experience.tsx"
    }
  ],
  "docs": "https://heyompatil.me/components/work-experience-component",
  "categories": [
    "marketing"
  ],
  "type": "registry:component"
}