diff -ruN date-6.x-2.8-after-1039192-8/date/date.module date-6.x-2.8-after-endtime/date/date.module
--- date-6.x-2.8-after-1039192-8/date/date.module	2012-01-11 12:19:06.000000000 +0000
+++ date-6.x-2.8-after-endtime/date/date.module	2012-01-21 10:14:53.000000000 +0000
@@ -106,7 +106,7 @@
   if (empty($item['value'])) {
     return TRUE;
   }
-  elseif($field['todate'] == 'required' && empty($item['value2'])) {
+  elseif(strpos($field['todate'], 'required') !== FALSE && empty($item['value2'])) {
     return TRUE;
   }
   return FALSE;
diff -ruN date-6.x-2.8-after-1039192-8/date/date_admin.inc date-6.x-2.8-after-endtime/date/date_admin.inc
--- date-6.x-2.8-after-1039192-8/date/date_admin.inc	2012-01-11 12:19:06.000000000 +0000
+++ date-6.x-2.8-after-endtime/date/date_admin.inc	2012-01-21 10:14:53.000000000 +0000
@@ -240,6 +240,9 @@
       if ($field['tz_handling'] != 'none' && !in_array('hour', array_filter($field['granularity']))) {
         form_set_error('tz_handling', t('Dates without hours granularity must not use any timezone handling.'));
       }
+      if (strpos($field['todate'], 'timeonly') !== FALSE && !in_array('hour', array_filter($field['granularity']))) {
+        form_set_error('granularity', t('Dates without hours granularity must not use a "Time only" To Date setting.'));
+      }
       break;
 
     case 'save':
@@ -345,11 +348,13 @@
     $form['repeat'] = array('#type' => 'hidden', '#value' => 0);
   }
 
-  $description = t("Display a matching second date field as a 'To date'. If marked 'Optional' field will be presented but not required. If marked 'Required' the 'To date' will be required if the 'From date' is required or filled in.");
+  $description = t("Display a matching second date field as a 'To date'. If marked 'Optional' field will be presented but not required. If marked 'Required' the 'To date' will be required if the 'From date' is required or filled in.<br/>If a 'Time only' option is chosen, then you will not be able to select a year, month or date for the 'To date', and these values will instead be set to the same as the 'From date' (or the day after if the time is after midnight). You must include Hours in the granularity if you select a 'Time only' option.");
   $description .= date_data_loss_warning('To date');
   $form['input']['todate'] = array(
     '#type' => 'radios', '#title' => t('To Date'),
-    '#options' => array('' => t('Never'), 'optional' => t('Optional'), 'required' => t('Required')),
+    '#options' => array('' => t('Never'),
+                        'optional' => t('Optional'), 'optional_timeonly' => t('Optional (time only)'),
+                        'required' => t('Required'), 'required_timeonly' => t('Required (time only)')),
     '#description' => $description,
     '#default_value' => isset($field['todate']) ? $field['todate'] : '',
     );
@@ -714,4 +719,4 @@
       );  
   }
   return $form;
-}
\ No newline at end of file
+}
diff -ruN date-6.x-2.8-after-1039192-8/date/date_content_generate.inc date-6.x-2.8-after-endtime/date/date_content_generate.inc
--- date-6.x-2.8-after-1039192-8/date/date_content_generate.inc	2012-01-11 12:19:06.000000000 +0000
+++ date-6.x-2.8-after-endtime/date/date_content_generate.inc	2012-01-21 10:14:53.000000000 +0000
@@ -29,9 +29,11 @@
   date_increment_round($start, $increment);
 
 	// Modify To date by 1 hour to 3 days, shorter for repeating dates
-	// longer for others.
+	// longer for others. Limit to 20 hours (just under one day) if
+	// we have a 'Time only' To Date.
 	$start2 = drupal_clone($start);
-	$max = !empty($field['repeat']) ? 720 : 4320;
+	$max = !empty($field['repeat']) ? 720
+	                                : (strpos($field['todate'], 'timeonly') !== FALSE ? 1200 : 4320);
 	$max = 240;
   date_modify($start2, '+'. mt_rand(60, $max) .' minutes');  
   date_increment_round($start2, $increment);
@@ -182,4 +184,4 @@
     }
   }
   return $options;
-}
\ No newline at end of file
+}
diff -ruN date-6.x-2.8-after-1039192-8/date/date_elements.inc date-6.x-2.8-after-endtime/date/date_elements.inc
--- date-6.x-2.8-after-1039192-8/date/date_elements.inc	2012-01-11 12:19:06.000000000 +0000
+++ date-6.x-2.8-after-endtime/date/date_elements.inc	2012-01-21 10:14:53.000000000 +0000
@@ -27,7 +27,7 @@
           form_set_error($error_field, t("A 'From date' date is required for field %field %delta.", array('%delta' => $field['multiple'] ? intval($delta + 1) : '', '%field' => t($field['widget']['label']))));
         }
         if ($processed == 'value2'
-          && $field['todate'] == 'required' && ($field['required']
+          && strpos($field['todate'], 'required') !== FALSE && ($field['required']
           && date_is_valid($item['value'], $field['type'], $field['granularity'])
           && !date_is_valid($item['value2'], $field['type'], $field['granularity']))) {
           form_set_error($error_field, t("A 'To date' is required for field %field %delta.", array('%delta' => $field['multiple'] ? intval($delta + 1) : '', '%field' => t($field['widget']['label']))));
@@ -268,7 +268,7 @@
   $offset_field = 'offset';
   $offset_field2 = 'offset2';
 
-  if ($field['todate'] != 'required' && !empty($element['#default_value'][$to_field]) && $element['#default_value'][$to_field] == $element['#default_value'][$from_field]) {
+  if (strpos($field['todate'], 'required') === FALSE && !empty($element['#default_value'][$to_field]) && $element['#default_value'][$to_field] == $element['#default_value'][$from_field]) {
     unset($element['#default_value'][$to_field]);
   }
 
@@ -317,9 +317,15 @@
 
   if (!empty($field['todate'])) {
     $element['#date_float'] = TRUE;
-    $element[$from_field]['#title']  = t('From date');
     $element[$to_field] = $element[$from_field];
-    $element[$to_field]['#title'] = t('To date');
+    if (strpos($field['todate'], 'timeonly') !== FALSE) {
+      $element[$from_field]['#title'] = t('Date and start time');
+      $element[$to_field]['#title'] = t('End time');
+      $element[$to_field]['#date_format'] = date_limit_format($element[$to_field]['#date_format'], array('hour','minute','second'));
+    } else {
+      $element[$from_field]['#title'] = t('From date');
+      $element[$to_field]['#title'] = t('To date');
+    }
     $element[$to_field]['#default_value'] = isset($element['#value'][$to_field]) ? $element['#value'][$to_field] : '';
     $element[$to_field]['#required'] = false;
     $element[$to_field]['#weight'] += .1;
@@ -429,17 +435,24 @@
 
     // Check todate input for blank values and substitute in fromdate
     // values where needed, then re-compute the todate with those values.
+    $timeonly = strpos($field['todate'], 'timeonly') !== FALSE;
     if ($field['todate']) {
       $merged_date = array();
       $to_date_empty = TRUE;
       foreach ($posted[$to_field] as $part => $value) {
-        $to_date_empty = $to_date_empty && empty($value) && !is_numeric($value);
-        $merged_date[$part] = empty($value) && !is_numeric($value) ? $posted[$from_field][$part] : $value;
-        if ($part == 'ampm' && $merged_date['ampm'] == 'pm' && $merged_date['hour'] < 12) {
-          $merged_date['hour'] += 12;
-        }
-        elseif ($part == 'ampm' && $merged_date['ampm'] == 'am' && $merged_date['hour'] == 12) {
-          $merged_date['hour'] -= 12;
+        if ($timeonly && ($part=='date' || $part=='day' || $part=='month' || $part=='year')) {
+          // force end date (not time) to match start date
+          // Note: $to_date_empty is deliberately only checked on the remaining time fields
+          $merged_date[$part] = $posted[$from_field][$part];
+        } else {
+          $to_date_empty = $to_date_empty && empty($value) && !is_numeric($value);
+          $merged_date[$part] = empty($value) && !is_numeric($value) ? $posted[$from_field][$part] : $value;
+          if ($part == 'ampm' && $merged_date['ampm'] == 'pm' && $merged_date['hour'] < 12) {
+            $merged_date['hour'] += 12;
+          }
+          elseif ($part == 'ampm' && $merged_date['ampm'] == 'am' && $merged_date['hour'] == 12) {
+            $merged_date['hour'] -= 12;
+          }
         }
       }
 
@@ -447,11 +460,14 @@
       // an error on the first element. We don't want to create 
       // duplicate messages on every date part, so the error will 
       // only go on the first.  
-      if ($to_date_empty && $field['todate'] == 'required') {
+      if ($to_date_empty && strpos($field['todate'], 'required') !== FALSE) {
         $errors[] = t('Some value must be entered in the To date.');
       }
 
       $element[$to_field]['#value'] = $merged_date;
+      if ($timeonly) {
+        $element[$to_field]['#date_format'] = $element[$from_field]['#date_format'];
+      }
 
       // Call the right function to turn this altered user input into
       // a new value for the todate.
@@ -475,11 +491,20 @@
       $item = date_element_empty($element, $form_state);
       $errors[] = t('The dates are invalid.');
     }
-    elseif (!empty($field['todate']) && $from_date > $to_date) {
+    elseif (!empty($field['todate']) && $from_date > $to_date && !$timeonly) {
       form_set_value($element[$to_field], $to_date, $form_state);
       $errors[] = t('The To date must be greater than the From date.');
     }
     else {
+      if ($timeonly) {
+        // In a time only To date mode, we have already forced the date part of
+        // to_date to be the same as from_date. Therefore, if from_date >
+        // to_date, this means that we have a post-midnight time, and want to
+        // increment to_date by one. This is done by setting a flag here, and
+        // doing the calculation later.
+        $inc_to_by_one_day = ($from_date > $to_date);
+      }
+
       // Convert input dates back to their UTC values and re-format to ISO
       // or UNIX instead of the DATETIME format used in element processing.
       $timezone = !empty($item[$tz_field]) ? $item[$tz_field] : $element['#date_timezone'];
@@ -491,6 +516,9 @@
       $item[$offset_field] = date_offset_get($from_date);
 
       $to_date = date_make_date($to_date, $timezone);
+      if ($timeonly && $inc_to_by_one_day) {
+        $to_date->modify('+1 day');
+      }
       $test_from = date_format($from_date, 'r');
       $test_to = date_format($to_date, 'r');
       $item[$offset_field2] = date_offset_get($to_date);
diff -ruN date-6.x-2.8-after-1039192-8/date_popup/date_popup.module date-6.x-2.8-after-endtime/date_popup/date_popup.module
--- date-6.x-2.8-after-1039192-8/date_popup/date_popup.module	2012-01-21 10:14:24.000000000 +0000
+++ date-6.x-2.8-after-endtime/date_popup/date_popup.module	2012-01-21 10:15:44.000000000 +0000
@@ -225,7 +225,25 @@
   $date = NULL;
   $granularity = date_format_order($element['#date_format']);
 
-  if (!empty($edit) && is_array($edit) && !empty($edit['date'])) {
+  /* For time-only "to" dates, we need to add a dummy date here, so that the syntax
+   * verification passes, and the time is not removed when clicking "Add another item"
+   * because the time-only user input doesn't match the granularity.
+   * The dummy date chosen does not matter, since it is never seen by the user, and
+   * the correct one will be calculated on node save. Therefore, for simplicity, we
+   * copy the date from the "from" date field.
+   */
+  $field_name = $element['#parents'][0];
+  $index = $element['#parents'][1];
+  $date_component = $element['#parents'][2];
+  $is_to_date = ($date_component == 'value2');
+  $time_only = (strpos($element['#field']['todate'], 'timeonly') !== FALSE);
+  $dummy_to_date = ($is_to_date && $time_only);
+  if ($dummy_to_date)
+  {
+    $element['#value']['date'] = $form_state['values'][$field_name][$index]['value']['date'];
+  }
+
+  if (!empty($edit) && is_array($edit) && ($dummy_to_date || !empty($edit['date']))) {
     $datetime = date_popup_input_value($element);
     $date = date_make_date($datetime, $element['#date_timezone'], DATE_DATETIME, $granularity);
   }
@@ -257,7 +275,6 @@
   $date_granularity = array_intersect($granularity, array('month', 'day', 'year'));
   $time_granularity = array_intersect($granularity, array('hour', 'minute', 'second'));
   $date_format = (date_limit_format($element['#date_format'], $date_granularity));
-  if (empty($date_granularity)) return array();
 
   // The datepicker can't handle zero or negative values like 0:+1
   // even though the Date API can handle them, so rework the value
@@ -298,7 +315,7 @@
   // the parent value
   $parents = array_merge($element['#parents'], array('date'));
   $sub_element = array(
-    '#type' => 'textfield',
+    '#type' => (empty($date_granularity) ? 'hidden' : 'textfield'),
     '#default_value' => (!empty($element['#value']['date']) || !empty($edit['date'])) && is_object($date) ? date_format_date($date, 'custom', $date_format) : '',
     '#id' => $id,    
     '#input' => FALSE,
