{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "project-card",
  "title": "Project Card",
  "author": "ilinxa",
  "description": "Project and case-study card with editorial status states and grid or feature layouts — overlay links and soft-failure item handling.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "button"
  ],
  "files": [
    {
      "path": "src/registry/components/data/project-card/project-card.tsx",
      "content": "\"use client\";\n\nimport { memo, useCallback, useId, useMemo, type MouseEvent } from \"react\";\nimport {\n  DEFAULT_PROJECT_CARD_LABELS,\n  type ProjectCardLabels,\n  type ProjectCardProps,\n  type ProjectCardItem,\n} from \"./types\";\nimport { ProjectCardGrid } from \"./parts/grid\";\nimport { ProjectCardFeature } from \"./parts/feature\";\n\nfunction ProjectCardInner({\n  project,\n  variant,\n  href,\n  getHref,\n  onClick,\n  linkComponent = \"a\",\n  categoryStyles,\n  labels: labelsProp,\n  titleClassName,\n  imageClassName,\n  className,\n  ariaLabel,\n  actions,\n  loading = \"lazy\",\n}: ProjectCardProps) {\n  const titleId = useId();\n\n  // The internal parts still use a positional shape for the click handler\n  // they pass to <a onClick={...}>. Adapt the public object-shape callback\n  // to a positional one at this boundary; parts stay untouched.\n  const handlePartClick = useCallback(\n    (proj: ProjectCardItem, mouseEvent: MouseEvent) => {\n      onClick?.({ project: proj, mouseEvent });\n    },\n    [onClick],\n  );\n\n  const labels = useMemo<Required<ProjectCardLabels>>(\n    () => ({ ...DEFAULT_PROJECT_CARD_LABELS, ...labelsProp }),\n    [labelsProp],\n  );\n\n  // href precedence: getHref > href > project.href > '#'\n  const resolvedHref =\n    (getHref ? getHref(project) : undefined) ?? href ?? project.href ?? \"#\";\n\n  const categoryStyle = categoryStyles?.[project.category];\n  const featured = project.featured === true;\n\n  const sharedProps = {\n    project,\n    status: project.status,\n    featured,\n    labels,\n    categoryStyle,\n    href: resolvedHref,\n    linkComponent,\n    onClick: onClick ? handlePartClick : undefined,\n    ariaLabel,\n    titleId,\n    titleClassName,\n    imageClassName,\n    className,\n    actions,\n    loading,\n  };\n\n  if (variant === \"feature\") {\n    return <ProjectCardFeature {...sharedProps} />;\n  }\n\n  return <ProjectCardGrid {...sharedProps} />;\n}\n\nexport const ProjectCard = memo(ProjectCardInner);\nProjectCard.displayName = \"ProjectCard\";\n\nexport default ProjectCard;\n",
      "type": "registry:component",
      "target": "components/project-card/project-card.tsx"
    },
    {
      "path": "src/registry/components/data/project-card/index.ts",
      "content": "export { ProjectCard, default } from \"./project-card\";\n\nexport type {\n  ProjectCardItem,\n  ProjectCardLabels,\n  ProjectCardProps,\n  ProjectCardVariant,\n  ProjectCategoryStyle,\n  ProjectStatus,\n} from \"./types\";\n\nexport { DEFAULT_PROJECT_CARD_LABELS } from \"./types\";\n\nexport {\n  PROJECT_STATUS_CONFIG,\n  type ProjectStatusConfigEntry,\n} from \"./lib/project-status\";\n\n",
      "type": "registry:component",
      "target": "components/project-card/index.ts"
    },
    {
      "path": "src/registry/components/data/project-card/types.ts",
      "content": "import type { ComponentType, ElementType, MouseEvent, ReactNode } from \"react\";\nimport type { ProjectStatus } from \"./lib/project-status\";\n\nexport type { ProjectStatus };\n\nexport type ProjectCardVariant = \"grid\" | \"feature\";\n\nexport interface ProjectCardItem {\n  /** Stable identifier. Used for React keys and the default ariaLabel. */\n  id: string;\n  /** Headline. Rendered as <h3>. Required. */\n  title: string;\n  /** Editorial category (e.g. \"Urban Renewal\"). Used as a key into `categoryStyles`. Required. */\n  category: string;\n  /** Image URL. Required (can be empty string — fallback placeholder rendered when falsy). */\n  image: string;\n  /** Image alt-text. Optional — falls back to `title`. */\n  imageAlt?: string;\n  /** Short summary. Required. Both variants line-clamp it. */\n  description: string;\n  /** Editorial status. Required — drives the status pill. */\n  status: ProjectStatus;\n  /** Free-form location string (\"Istanbul, Kadıköy\", \"Remote\", etc). Optional — meta line omitted if missing. */\n  location?: string;\n  /** Free-form year string (\"2023\", \"Q4 2023\", \"2023–2025\" ranges). Optional — meta line omitted if missing. */\n  year?: string;\n  /** Default href for this project. Optional — overridden by `href` / `getHref` on the card. */\n  href?: string;\n  /** Promotional flag. Optional — adds visual lift treatment + Star title prefix. */\n  featured?: boolean;\n}\n\nexport interface ProjectCardLabels {\n  // ─── Status pill ────────────────────────────────────────────────\n  /** Default: 'Completed'. */\n  completed?: string;\n  /** Default: 'In progress'. */\n  ongoing?: string;\n  /** Default: 'Planned'. */\n  planned?: string;\n  // ─── Image-area CTA (grid only) ─────────────────────────────────\n  /** Hover-reveal CTA pill text. Default: 'View details'. */\n  viewDetails?: string;\n  // ─── A11y ───────────────────────────────────────────────────────\n  /** sr-only label appended after the title for featured projects. Default: 'Featured project'. */\n  featuredAriaLabel?: string;\n}\n\nexport interface ProjectCategoryStyle {\n  /** Tailwind class string for the category pill. Optional. */\n  className?: string;\n  /** Custom icon overriding the default `Building2`. Optional. */\n  icon?: ComponentType<{ className?: string; \"aria-hidden\"?: boolean | \"true\" | \"false\" }>;\n}\n\nexport interface ProjectCardProps {\n  /** The project to render. */\n  project: ProjectCardItem;\n\n  /** Visual variant. Required — no default; explicit per render site. */\n  variant: ProjectCardVariant;\n\n  // ─── Navigation ──────────────────────────────────────────────────\n  /** URL the card links to. Mutually exclusive with `getHref`. */\n  href?: string;\n  /** Alternative href derivation. Receives the project, returns a URL. Wins over `href` and `project.href`. */\n  getHref?: (project: ProjectCardItem) => string;\n  /**\n   * Click handler — object-shape, fired before navigation if `href` is also\n   * set. v0.2 cutover from positional `(project, mouseEvent)`.\n   */\n  onClick?: (args: { project: ProjectCardItem; mouseEvent: MouseEvent }) => void;\n  /** Element used for the link. Default: 'a'. Pass NextLink / RemixLink / etc. */\n  linkComponent?: ElementType;\n\n  // ─── Theming ─────────────────────────────────────────────────────\n  /** Map of category → className + icon override. Default: empty (universal Building2 + neutral chip). */\n  categoryStyles?: Record<string, ProjectCategoryStyle>;\n  /** Localized labels. Defaults are English. */\n  labels?: ProjectCardLabels;\n  /** Override classes for the title. */\n  titleClassName?: string;\n  /** Override classes for the image. */\n  imageClassName?: string;\n  /** Override classes for the root <article>. */\n  className?: string;\n\n  // ─── Accessibility ───────────────────────────────────────────────\n  /** Override the link's accessible name. Default: title (via aria-labelledby). */\n  ariaLabel?: string;\n\n  // ─── Nested interactives (overlay-link pattern) ──────────────────\n  /** Optional cluster of buttons/links that sit ABOVE the link overlay (z-10). */\n  actions?: ReactNode;\n\n  // ─── Performance ─────────────────────────────────────────────────\n  /** Image loading strategy. Default: 'lazy'. */\n  loading?: \"lazy\" | \"eager\";\n}\n\n/** Default English labels — exported for consumer composition. */\nexport const DEFAULT_PROJECT_CARD_LABELS: Required<ProjectCardLabels> = {\n  completed: \"Completed\",\n  ongoing: \"In progress\",\n  planned: \"Planned\",\n  viewDetails: \"View details\",\n  featuredAriaLabel: \"Featured project\",\n};\n",
      "type": "registry:component",
      "target": "components/project-card/types.ts"
    },
    {
      "path": "src/registry/components/data/project-card/parts/grid.tsx",
      "content": "\"use client\";\n\nimport type { ElementType, MouseEvent, ReactNode } from \"react\";\nimport { ArrowRight, Building2, Calendar, MapPin, Star } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport type {\n  ProjectCardLabels,\n  ProjectCardItem,\n  ProjectCategoryStyle,\n} from \"../types\";\nimport {\n  PROJECT_STATUS_CONFIG,\n  type ProjectStatus,\n} from \"../lib/project-status\";\nimport { ImageFallback } from \"../lib/image-fallback\";\n\ninterface GridPartProps {\n  project: ProjectCardItem;\n  status: ProjectStatus;\n  featured: boolean;\n  labels: Required<ProjectCardLabels>;\n  categoryStyle?: ProjectCategoryStyle;\n  href: string;\n  linkComponent: ElementType;\n  onClick?: (project: ProjectCardItem, mouseEvent: MouseEvent) => void;\n  ariaLabel?: string;\n  titleId: string;\n  titleClassName?: string;\n  imageClassName?: string;\n  className?: string;\n  actions?: ReactNode;\n  loading: \"lazy\" | \"eager\";\n}\n\nexport function ProjectCardGrid({\n  project,\n  status,\n  featured,\n  labels,\n  categoryStyle,\n  href,\n  linkComponent: LinkComponent,\n  onClick,\n  ariaLabel,\n  titleId,\n  titleClassName,\n  imageClassName,\n  className,\n  actions,\n  loading,\n}: GridPartProps) {\n  const statusEntry = PROJECT_STATUS_CONFIG[status];\n  const CategoryIcon = categoryStyle?.icon ?? Building2;\n  const hasActions = actions != null;\n  const hasMeta = project.location != null || project.year != null;\n\n  const handleClick = onClick\n    ? (e: MouseEvent<HTMLAnchorElement>) => onClick(project, e)\n    : undefined;\n\n  return (\n    <article\n      className={cn(\n        \"relative group bg-card rounded-2xl overflow-hidden shadow-sm motion-safe:hover:shadow-xl motion-safe:hover:-translate-y-2 transition-all duration-500 border border-border h-full flex flex-col\",\n        featured && \"border-t-4 border-t-primary\",\n        className,\n      )}\n    >\n      {/* Image area */}\n      <div className=\"relative aspect-16/10 overflow-hidden\">\n        {project.image ? (\n          <img\n            src={project.image}\n            alt={project.imageAlt ?? project.title}\n            loading={loading}\n            className={cn(\n              \"w-full h-full object-cover motion-safe:group-hover:scale-110 transition-transform duration-700\",\n              imageClassName,\n            )}\n          />\n        ) : (\n          <ImageFallback className={imageClassName} />\n        )}\n\n        {/* Subtle dark gradient overlay for badge legibility (replaces source's aggressive lime tint that competed with image content) */}\n        <div\n          aria-hidden=\"true\"\n          className=\"absolute inset-0 bg-linear-to-t from-black/60 via-black/15 to-transparent motion-safe:group-hover:from-black/70 transition-all duration-300\"\n        />\n\n        {/* Status pill — top-left (always) */}\n        <span\n          className={cn(\n            \"absolute top-4 left-4 inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium\",\n            statusEntry.className,\n          )}\n        >\n          {labels[status]}\n        </span>\n\n        {/* Category pill — top-right OR bottom-right when actions yields top */}\n        <span\n          className={cn(\n            \"absolute inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium backdrop-blur-sm\",\n            hasActions ? \"bottom-4 right-4\" : \"top-4 right-4\",\n            categoryStyle?.className ?? \"bg-white/20 text-white\",\n          )}\n        >\n          <CategoryIcon aria-hidden=\"true\" className=\"w-3 h-3 mr-1.5\" />\n          {project.category}\n        </span>\n\n        {/* Actions slot — top-right, above link overlay */}\n        {hasActions && (\n          <div className=\"absolute top-4 right-4 z-10 flex gap-1.5\">\n            {actions}\n          </div>\n        )}\n\n        {/* Hover-reveal \"View details\" CTA — center, decorative (clicks pass through to link) */}\n        <div\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute inset-0 flex items-center justify-center opacity-0 motion-safe:group-hover:opacity-100 transition-opacity duration-300\"\n        >\n          <span className=\"inline-flex items-center gap-2 px-6 py-3 bg-primary text-primary-foreground rounded-full font-medium shadow-md\">\n            {labels.viewDetails}\n            <ArrowRight className=\"w-4 h-4 motion-safe:group-hover:translate-x-1 transition-transform rtl:rotate-180\" />\n          </span>\n        </div>\n      </div>\n\n      {/* Link overlay — covers whole article, transparent, on top of content */}\n      <LinkComponent\n        href={href}\n        aria-labelledby={ariaLabel ? undefined : titleId}\n        aria-label={ariaLabel}\n        onClick={handleClick}\n        className=\"absolute inset-0 z-0 rounded-2xl focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 outline-none\"\n      />\n\n      {/* Content */}\n      <div className=\"p-6 flex-1 flex flex-col\">\n        <h3\n          id={titleId}\n          className={cn(\n            \"text-xl font-semibold text-foreground mb-3 motion-safe:group-hover:text-primary transition-colors line-clamp-2\",\n            titleClassName,\n          )}\n        >\n          {featured && (\n            <Star\n              aria-hidden=\"true\"\n              className=\"inline w-4 h-4 fill-primary text-primary mr-1.5 align-baseline\"\n            />\n          )}\n          {project.title}\n          {featured && (\n            <span className=\"sr-only\"> ({labels.featuredAriaLabel})</span>\n          )}\n        </h3>\n\n        <p className=\"text-muted-foreground text-sm mb-4 line-clamp-2\">\n          {project.description}\n        </p>\n\n        {hasMeta && (\n          <ul\n            role=\"list\"\n            className=\"flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-muted-foreground mt-auto\"\n          >\n            {project.location != null && (\n              <li className=\"inline-flex items-center gap-1.5\">\n                <MapPin aria-hidden=\"true\" className=\"w-4 h-4 text-muted-foreground/70\" />\n                <span>{project.location}</span>\n              </li>\n            )}\n            {project.year != null && (\n              <li className=\"inline-flex items-center gap-1.5\">\n                <Calendar aria-hidden=\"true\" className=\"w-4 h-4 text-muted-foreground/70\" />\n                <span>{project.year}</span>\n              </li>\n            )}\n          </ul>\n        )}\n      </div>\n    </article>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/project-card/parts/grid.tsx"
    },
    {
      "path": "src/registry/components/data/project-card/parts/feature.tsx",
      "content": "\"use client\";\n\nimport type { ElementType, MouseEvent, ReactNode } from \"react\";\nimport { Building2, Star } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport type {\n  ProjectCardLabels,\n  ProjectCardItem,\n  ProjectCategoryStyle,\n} from \"../types\";\nimport {\n  PROJECT_STATUS_CONFIG,\n  type ProjectStatus,\n} from \"../lib/project-status\";\nimport { ImageFallback } from \"../lib/image-fallback\";\n\ninterface FeaturePartProps {\n  project: ProjectCardItem;\n  status: ProjectStatus;\n  featured: boolean;\n  labels: Required<ProjectCardLabels>;\n  categoryStyle?: ProjectCategoryStyle;\n  href: string;\n  linkComponent: ElementType;\n  onClick?: (project: ProjectCardItem, mouseEvent: MouseEvent) => void;\n  ariaLabel?: string;\n  titleId: string;\n  titleClassName?: string;\n  imageClassName?: string;\n  className?: string;\n  actions?: ReactNode;\n  loading: \"lazy\" | \"eager\";\n}\n\nexport function ProjectCardFeature({\n  project,\n  status,\n  featured,\n  labels,\n  categoryStyle,\n  href,\n  linkComponent: LinkComponent,\n  onClick,\n  ariaLabel,\n  titleId,\n  titleClassName,\n  imageClassName,\n  className,\n  actions,\n  loading,\n}: FeaturePartProps) {\n  const statusEntry = PROJECT_STATUS_CONFIG[status];\n  const CategoryIcon = categoryStyle?.icon ?? Building2;\n  const hasActions = actions != null;\n\n  const handleClick = onClick\n    ? (e: MouseEvent<HTMLAnchorElement>) => onClick(project, e)\n    : undefined;\n\n  return (\n    <article\n      className={cn(\n        \"relative group rounded-xl overflow-hidden cursor-pointer h-full\",\n        featured && \"ring-2 ring-primary ring-inset\",\n        className,\n      )}\n    >\n      {/* Background image (uses <img> for alt + lazy + error semantics) */}\n      {project.image ? (\n        <img\n          src={project.image}\n          alt={project.imageAlt ?? project.title}\n          loading={loading}\n          className={cn(\n            \"absolute inset-0 w-full h-full object-cover motion-safe:group-hover:scale-110 transition-transform duration-500\",\n            imageClassName,\n          )}\n        />\n      ) : (\n        <ImageFallback className={cn(\"absolute inset-0\", imageClassName)} />\n      )}\n\n      {/* Neutral-darken gradient overlay (NOT primary-tinted; differs from grid deliberately) */}\n      <div\n        aria-hidden=\"true\"\n        className=\"absolute inset-0 bg-linear-to-t from-black/90 via-black/40 to-transparent opacity-80 motion-safe:group-hover:opacity-90 transition-opacity duration-300\"\n      />\n\n      {/* Top-left cluster: category (always) + status (only when actions yielded top-right) */}\n      <div className=\"absolute top-3 left-3 flex flex-col items-start gap-1.5\">\n        <span\n          className={cn(\n            \"inline-flex items-center text-xs px-2 py-1 rounded-full backdrop-blur-sm\",\n            categoryStyle?.className ?? \"bg-white/20 text-white\",\n          )}\n        >\n          <CategoryIcon aria-hidden=\"true\" className=\"w-3 h-3 mr-1\" />\n          {project.category}\n        </span>\n        {hasActions && (\n          <span\n            className={cn(\n              \"inline-flex items-center text-xs px-2.5 py-1 rounded-full backdrop-blur-sm\",\n              statusEntry.className,\n            )}\n          >\n            {labels[status]}\n          </span>\n        )}\n      </div>\n\n      {/* Top-right: actions (when supplied) OR status pill */}\n      {hasActions ? (\n        <div className=\"absolute top-3 right-3 z-10 flex gap-1.5\">\n          {actions}\n        </div>\n      ) : (\n        <span\n          className={cn(\n            \"absolute top-3 right-3 inline-flex items-center text-xs px-2.5 py-1 rounded-full backdrop-blur-sm\",\n            statusEntry.className,\n          )}\n        >\n          {labels[status]}\n        </span>\n      )}\n\n      {/* Link overlay — covers whole article */}\n      <LinkComponent\n        href={href}\n        aria-labelledby={ariaLabel ? undefined : titleId}\n        aria-label={ariaLabel}\n        onClick={handleClick}\n        className=\"absolute inset-0 z-0 rounded-xl focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 outline-none\"\n      />\n\n      {/* Bottom content block (white-on-dark, no meta row by design) */}\n      <div className=\"absolute bottom-0 left-0 right-0 p-4 text-white\">\n        <h3\n          id={titleId}\n          className={cn(\n            \"font-bold text-base mb-1 text-white motion-safe:group-hover:text-primary transition-colors duration-300 line-clamp-2\",\n            titleClassName,\n          )}\n        >\n          {featured && (\n            <Star\n              aria-hidden=\"true\"\n              className=\"inline w-4 h-4 fill-primary text-primary mr-1.5 align-baseline\"\n            />\n          )}\n          {project.title}\n          {featured && (\n            <span className=\"sr-only\"> ({labels.featuredAriaLabel})</span>\n          )}\n        </h3>\n        <p className=\"text-xs text-white/80 line-clamp-2 motion-safe:group-hover:text-white/90 transition-colors\">\n          {project.description}\n        </p>\n      </div>\n    </article>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/project-card/parts/feature.tsx"
    },
    {
      "path": "src/registry/components/data/project-card/lib/project-status.ts",
      "content": "export type ProjectStatus = \"completed\" | \"ongoing\" | \"planned\";\n\nexport interface ProjectStatusConfigEntry {\n  /** English default label. Overridable via the `labels` prop on the card. */\n  label: string;\n  /** Tailwind class string for the status pill background + foreground. */\n  className: string;\n}\n\n/**\n * Public status kernel. Pure data — no React imports, no DOM access.\n * Tree-shakeable + server-component-importable. Consumers can read the same\n * label / className map for status legends, filter rows, count summaries —\n * without rendering the card.\n *\n * Status is editorial — set on the data object by an editor, NOT derived from\n * a clock. There is intentionally NO `getProjectStatus(project, now)` helper.\n */\nexport const PROJECT_STATUS_CONFIG: Record<ProjectStatus, ProjectStatusConfigEntry> = {\n  completed: {\n    label: \"Completed\",\n    className: \"bg-primary text-primary-foreground\",\n  },\n  ongoing: {\n    // chart-3 (teal) instead of `bg-accent` — pro-ui's `--accent` is a near-white\n    // surface token (NOT a brand color), which renders status pills invisibly.\n    // Teal sits between completed (lime) and planned (grey) and reads as \"active\".\n    label: \"In progress\",\n    className: \"bg-chart-3 text-white\",\n  },\n  planned: {\n    label: \"Planned\",\n    className: \"bg-muted text-muted-foreground border border-border\",\n  },\n};\n",
      "type": "registry:component",
      "target": "components/project-card/lib/project-status.ts"
    },
    {
      "path": "src/registry/components/data/project-card/lib/image-fallback.tsx",
      "content": "import { Building2 } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nexport function ImageFallback({ className }: { className?: string }) {\n  return (\n    <div\n      className={cn(\n        \"flex h-full w-full items-center justify-center bg-muted\",\n        className,\n      )}\n    >\n      <Building2\n        aria-hidden=\"true\"\n        className=\"h-8 w-8 text-muted-foreground/40\"\n      />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/project-card/lib/image-fallback.tsx"
    }
  ],
  "categories": [
    "data",
    "projects"
  ],
  "type": "registry:block"
}